数据专栏

智能大数据搬运工,你想要的我们都有

科技资讯:

科技学院:

科技百科:

科技书籍:

网站大全:

软件大全:

Redis中当内存达到极限时,主要采用了 6种内存淘汰策略/方式 进行内存对象的释放操作。 V olatile-lru:从设置了过期时间的数据集中,选择最近最少使用的数据释放。 A llkeys-lru:从数据集中(包括设置过期时间以及未设置过期时间的数据集中),选择最近最少使用的数据释放。 V olatile-random:从设置了过期时间的数据集中,随机选择一个数据进行释放。 A llkeys-random:从数据集中(包括了设置过期时间以及未设置过期时间的数据集)随机选择一个数据进行入释放。 V olatile-ttl:从设置了过期时间的数据集中,选择马上就要过期的数据进行释放。 N oeviction:不删除任意数据(但Redis还会根据引用计数器进行释放),这时如果内存不够时,会直接返回错误。 参考文档: https://www.cnblogs.com/WJ5888/p/4371647.html
来源:OSCHINA
发布时间:2020-03-26 16:23:00
Redis使用的内存回收算法是 引用计数算法 和 LRU算法 。 1.引用计数算法: 对于创建的每一个对象都有一个与之关联的计数器,这个计数器记录着该对象被使用的次数,垃圾收集器在进行垃圾回收时,对扫描到的每一个对象判断一下计数器是否等于0,若等于0,就会释放该对象占用的内存空间,同时将该对象引用的其他对象的计数器进行减一操作。 算法实现方式: 引用计数算法的垃圾收集一般有侵入式与非侵入式两种,侵入式的实现就是将引用计数器直接根植在对象内部,用C++的思想进行解释就是,在对象的构造或者拷贝构造中进行加一操作,在对象的析构中进行减一操作;非侵入式思想就是有一块单独的内存区域,用作引用计数器。 算法优点 : 使用引用计数器,内存回收可以穿插在程序的运行中,在程序运行中,当发现某一对象的引用计数器为0时,可以立即对该对象所占用的内存空间进行回收,这种方式可以避免FULL GC(完全垃圾收集)时带来的程序暂停,Redis中就是在引用计数器为0时,对内存进行了回收。 算法缺点: 采用引用计数器进行垃圾回收,最大的缺点就是不能解决循环引用的问题,例如一个父对象持有一个子对象的引用,子对象也持有父对象的引用,这种情况下,父子对象将一直存在于JVM的堆中,无法进行回收。 2.LRU算法 : LRU是Least Recently Used的缩写,即最近最少使用,是一种常用的页面置换算法,选择最近最久未使用的页面予以淘汰。该算法赋予每个页面一个访问字段,用来记录该页面自上次被访问以来所经历的时间 t,当必须淘汰一个页面时,选择现有页面中其 t 值最大的,即最近最少使用的页面给予淘汰。 LRU算法最为经典的实现,就是 HashMap+Double LinkedList , 时间复杂度为 O(1) ,但是 如果按照HashMap和双向链表实现,需要额外的存储存放next和prev指针,牺牲比较大的存储空间,显然是不划算的。 所以Redis中的LRU算法 ,就是随机取出若干个key,然后按照访问时间排序后,淘汰掉最不经常使用的那个。 参考文档: https://my.oschina.net/u/4480939/blog/write https://www.cnblogs.com/WJ5888/p/4371647.html https://www.cnblogs.com/WJ5888/p/4359783.html
来源:OSCHINA
发布时间:2020-03-26 16:08:00
Kubernetes爆出中等严重性安全漏洞——Kubernetes API Server拒绝服务漏洞CVE-2019-1002100。 本文将进行漏洞解读和情景再现,并分享漏洞修复方案,Rancher用户来看应对之策了! CVE-2019-1002100漏洞 美国当地时间2019年3月2日,Kubernetes社区发布了Kubernetes API server拒绝服务的漏洞(CVE-2019-1002100),即有API写入权限的用户在写入资源时会导致Kubernetes API server过度消耗资源,此漏洞被评级为【中等严重性】。 此漏洞表现为用户在向Kubernetes API server发送 json-patch规则的补丁包来更新资源对象时(例如kubectl patch xxx --type json 或者“Content-Type: application/json-patch+json”),Kubernetes API server会消耗极大的资源,最终导致API server拒绝连接。 https://github.com/kubernetes/kubernetes/issues/74534 情景再现 一个json-patch的例子: kubectl patch deployment test --type='json' -p '[{"op": "add", "path": "/metadata/labels/test", "value": "test"},{"op": "add", "path": "/metadata/labels/app", "value": "test"} ,{…} ]' 当我们向Kubernetes频繁地发送多个json-patch请求来更新资源对象时,可以发现Kubernetes API server会消耗很多资源来处理我们的请求。 此时会有一部分资源的patch请求失败,无法得到Kubernetes API server的响应。 受此漏洞影响的Kubernetes API server的版本包括: v1.0.0 – 1.10.x v1.11.0 – 1.11.7 v1.12.0 – 1.12.5 v1.13.0 – 1.13.3 Kubernetes官方建议用户在升级至修复版本之前,可针对此漏洞的采取的缓解措施为: 对不受信任用户移除patch权限 漏洞修复 Kubernetes社区很快地修复了此漏洞,增加了对用户json-patch操作数量的限制。 当用户对某一资源对象修改的 json-patch 内容超过10000个操作时,Kubernetes API server会返回413(RequestEntityTooLarge)的错误。 错误信息如下: Request entity too large: The allowed maximum operations in a JSON patch is 10000, got 10004 修复的Kubernetes版本包括: v1.11.8 v1.12.6 v1.13.4 Rancher已发布最新版本应对此次漏洞 此次也一如既往,在Kubernetes自身爆出漏洞之后,Rancher Labs团队都第一时间响应,保障使用Rancher平台管理Kubernetes集群的用户的安全。 如果你是使用Rancher平台管理Kubernetes集群,不用担心,Rancher已于今日发布了最新版本,支持包含漏洞修复的Kubernetes版本,保障所有Rancher用户的Kubernetes集群不受此次漏洞困扰。 最新发布的Rancher版本为: v2.1.7(提供Kubernetes v1.11.8, v1.12.6, v1.13.4支持) v2.0.12(提供Kubernetes v1.11.8支持) 对于Rancher 1.6.x的用户,可以在Rancher v1.6.26的Catalog中使用Kubernetes发布的修复版本 v1.11.8和v1.12.6 此次漏洞会影响的Kubernetes版本范围较广,建议中招的用户尽快升级哟! 为用户的Docker & K8S之旅护航 Rancher Kubernetes平台拥有着超过一亿次下载量,我们深知安全问题对于用户而言的重要性,更遑论那些通过Rancher平台在生产环境中运行Docker及Kubernetes的数千万用户。 2018年年底Kubernetes被爆出的首个严重安全漏洞CVE-2018-1002105,就是由Rancher Labs联合创始人及首席架构师Darren Shepherd发现的。 2019年1月Kubernetes被爆出仪表盘和外部IP代理安全漏洞CVE-2018-18264时,Rancher Labs也是第一时间向用户响应,确保所有Rancher 2.x和1.6.x的用户都完全不被漏洞影响。 2019年2月爆出的严重的runc容器逃逸漏洞CVE-2019-5736,影响到大多数Docker与Kubernetes用户,Rancher Kubernetes管理平台和RancherOS操作系统均在不到一天时间内紧急更新,是业界第一个紧急发布新版本支持Docker补丁版本的平台,还帮忙将修复程序反向移植到所有版本的Docker并提供给用户,且提供了连Docker官方都不支持的针对Linux 3.x内核的修复方案。 负责、可靠、快速响应、以用户为中心,是Rancher始终不变的初心;在每一次业界出现问题时,严谨踏实为用户提供相应的应对之策,也是Rancher一如既往的行事之道。未来,Rancher也将一如既往支持与守护在用户的K8S之路左右,确保大家安全、稳妥、无虞地继续前进❤️
来源:OSCHINA
发布时间:2019-03-08 10:15:00
根据 Gartner 对全球 CIO 的调查结果显示,人工智能将成为 2019 年组织革命的颠覆性力量。对于人工智能来说,算力即正义,成本即能力,利用 Docker 和 Kubernetes 代表云原生技术为 AI 提供了一种新的工作模式,将 GPU 机器放到统一的资源池进行调度和管理,这避免了GPU 资源利用率低下和人工管理的成本。因此,全球主要的容器集群服务厂商 Kubernetes 都提供了 Nvidia GPU 容器集群调度能力,但是通常都是将一个 GPU 卡分配给一个容器。这虽然可以实现比较好的隔离性,确保使用 GPU 的应用不会被其他应用影响;对于深度学习模型训练的场景也非常适合,但是,针对模型开发和模型预测的场景还是会显得比较浪费。基于此,大家有了共享 GPU 的集群调度需求。 Kubernetes 共享 GPU 集群调度 共享 GPU 的集群调度就是能够让更多的模型开发和预测服务共享同一个 GPU 卡,进而提高集群中 Nvidia GPU 的利用率。而这就需要提供 GPU 资源的划分,而这里 GPU 资源划分的维度指的就是 GPU 显存和 Cuda Kernel 线程的划分。通常在集群级别谈支持共享 GPU 是以下两件事情: 1.调度 2.隔离,我们这里主要讨论的是调度,隔离的方案目前需要用户通过应用限制(比如使用 Tensorflow 的per_process_gpu_memory_fraction 来控制),未来会提供基于 Nvidia 的 MPS 的可选项, 也会考虑 GPU 的方案。 而对于细粒度的 GPU 卡调度,目前 Kubernetes 社区并没有很好的方案,这是由于 Kubernetes 对于 GPU 这类扩展资源的定义仅仅支持整数粒度的加加减减,无法支持复杂资源的分配。比如用户希望使用 Pod A 占用半张 GPU卡,这在目前 Kubernetes 的架构设计中无法实现资源分配的记录和调用。这里挑战是多卡 GPU 共享是实际矢量资源问题,而 Extened Resource 是标量资源的描述。 针对此问题,我们设计了一个 Out Of Tree 的共享 GPU 调度方案,该方案依赖于 Kubernetes 的现有的工作机制: Extended Resource 定义 Scheduler Extender 机制 Device Plugin 机制 Kubectl 的扩展机制 这个 GPU 共享调度扩展的好处是:利用 Kubernetes 的扩展和插件机制实现,对于 API Server,Scheduler,Controller Manager 以及 Kubelet 等核心组件没有侵入性。这就方便了使用者可以在不同 Kubernetes 版本上应用这个方案,无需 rebase 代码和重新构建 Kubernetes 二进制包。 用户场景 集群管理员:“我想提高集群的 GPU 使用率;在开发过程中,多个用户共享模型开发环境。” 应用开发人员:“我希望能够同时在 Volta GPU 上运行多个推理任务。” []( https://www.atatech.org/articles/132268#2) 目标 能够让使用者通过 API 描述对于一个可共享资源的申请, 并能实现该种资源的调度 []( https://www.atatech.org/articles/132268#3) 非目标 不支持该共享资源的隔离 不支持超卖 []( https://www.atatech.org/articles/132268#4) 设计原则 明确问题简化设计,第一步只负责调度和部署,后续再实现运行时显存管控。 有很多的客户明确的诉求是首先可以支持多AI应用可以调度到同一个 GPU 上,他们可以接受从应用级别控制显存的大小,利用类似 gpu_options.per_process_gpu_memory_fraction 控制应用的显存使用量。那我们要解决的问题就先简化到以显存为调度标尺,并且把显存使用的大小以参数的方式传递给容器内部。 不做侵入式修改 本设计中不会修改 Kubernetes 核心的 Extended Resource 的设计, Scheduler 的实现,Device Plugin 的机制以及 Kubelet 的相关设计。重用 Extended Resource 描述共享资源的申请 API。这样的好处在于提供一个可以移植的方案,用户可以在原生 Kubernetes 上使用这个方案。 按显存和按卡调度的方式可以在集群内并存,但是同一个节点内是互斥的,不支持二者并存;要么是按卡数目,要么是按显存分配。 详细设计 []( https://www.atatech.org/articles/132268#6) 前提: 依旧延用 Kubernetes Extended Resource 定义,但是衡量维度最小单位从 1 个 GPU 卡变为 GPU 显存的 MiB。如果所节点使用的 GPU 为单卡 16GiB 显存,它对应的资源就是 16276MiB; 由于用户对于共享GPU的诉求在于模型开发和模型预测场景,在此场景下,用户申请的GPU资源上限不会超过一张卡,也就是申请的资源上限为单卡。 而我们的工作首先是定义了两个新的 Extended Resource: 第一个是 gpu-mem, 对应的是 GPU 显存;第二个是 gpu-count,对应的是 GPU 卡数。 通过两个标量资源描述矢量资源, 并且结合这一资源,提供支持共享 GPU 的工作机制。下面是基本的架构图: []( https://www.atatech.org/articles/132268#7) 核心功能模块: GPU Share Scheduler Extender : 利用 Kubernetes 的调度器扩展机制,负责在全局调度器 Filter 和 Bind 的时候判断节点上单个 GPU 卡是否能够提供足够的 GPU Mem,并且在 Bind 的时刻将 GPU 的分配结果通过 annotation 记录到 Pod Spec 以供后续 Filter 检查分配结果。 GPU Share Device Plugin : 利用 Device Plugin 机制,在节点上被 Kubelet 调用负责 GPU 卡的分配,依赖 scheduler Extender 分配结果执行。 []( https://www.atatech.org/articles/132268#8) 具体流程: 资源上报 GPU Share Device Plugin 利用 nvml 库查询到 GPU 卡的数量和每张 GPU 卡的显存, 通过 ListAndWatch() 将节点的 GPU 总显存(数量 显存)作为另外 Extended Resource 汇报给 Kubelet; Kubelet 进一步汇报给 Kubernetes API Server。 举例说明,如果节点含有两块 GPU 卡,并且每块卡包含 16276MiB,从用户的角度来看:该节点的 GPU 资源为 16276 2 = 32552; 同时也会将节点上的 GPU 卡数量 2 作为另外一个 Extended Resource 上报。 2. 扩展调度 GPU Share Scheduler Extender 可以在分配 gpu-mem 给 Pod 的同时将分配信息以 annotation 的形式保留在 Pod spec 中,并且在过滤时刻根据此信息判断每张卡是否包含足够可用的 gpu-mem 分配。 2.1 Kubernetes 默认调度器在进行完所有过滤(filter)行为后会通过 http 方式调用 GPU Share Scheduler Extender的filter 方法, 这是由于默认调度器计算 Extended Resource 时,只能判断资源总量是否有满足需求的空闲资源,无法具体判断单张卡上是否满足需求;所以就需要由 GPU Share Scheduler Extender 检查单张卡上是否含有可用资源。 以下图为例, 在由 3 个包含两块 GPU 卡的节点组成的 Kubernetes 集群中,当用户申请 gpu-mem=8138 时,默认调度器会扫描所有节点,发现 N1 所剩的资源为 (16276 * 2 - 16276 -12207 = 4069 )不满足资源需求,N1 节点被过滤掉。 而 N2 和 N3 节点所剩资源都为 8138MiB,从整体调度的角度看,都符合默认调度器的条件;此时默认调度器会委托 GPU Share Scheduler Extender 进行二次过滤,在二次过滤中,GPU Share Scheduler Extender 需要判断单张卡是否满足调度需求,在查看 N2 节点时发现该节点虽然有 8138MiB 可用资源,但是落到每张卡上看,GPU0 和分别 GPU1 只有 4069MiB 的可用资源,无法满足单卡 8138MiB 的诉求。而 N3 节点虽然也是总共有 8138MiB 可用资源,但是这些可用资源都属于 GPU0,满足单卡可调度的需求。由此,通过 GPU Share Scheduler Extender 的筛选就可以实现精准的条件筛选。 2.2 当调度器找到满足条件的节点,就会委托 GPU Share Scheduler Extender 的 bind 方法进行节点和 Pod 的绑定,这里 Extender 需要做的是两件事情 以 binpack 的规则找到节点中最优选择的 GPU 卡 id,此处的最优含义是对于同一个节点不同的 GPU 卡,以 binpack 的原则作为判断条件,优先选择空闲资源满足条件但同时又是所剩资源最少的 GPU 卡,并且将其作为 ALIYUN_COM_GPU_MEM_IDX 保存到 Pod 的 annotation 中;同时也保存该 Pod 申请的 GPU Memory 作为 ALIYUN_COM_GPU_MEM_POD 和 ALIYUN_COM_GPU_MEM_ASSUME_TIME 保存至 Pod 的 annotation 中,并且在此时进行 Pod 和所选节点的绑定。 注意:这时还会保存 ALIYUN_COM_GPU_MEM_ASSIGNED 的 Pod annotation,它被初始化为“false”。它表示该 Pod 在调度时刻被指定到了某块 GPU 卡,但是并没有真正在节点上创建该 Pod。 ALIYUN_COM_GPU_MEM_ASSUME_TIME 代表了 指定 时间。 如果此时发现分配节点上没有 GPU 资源符合条件,此时不进行绑定,直接不报错退出,默认调度器会在 assume 超时后重新调度。 调用 Kubernetes API 执行节点和 Pod 的绑定 以下图为例,当 GPU Share Scheduler Extender 要把 gpu-mem:8138 的 Pod 和经过筛选出来的节点 N1 绑定,首先会比较不同 GPU 的可用资源,分别为 GPU0(12207),GPU1(8138),GPU2(4069),GPU3(16276),其中 GPU2 所剩资源不满足需求,被舍弃掉;而另外三个满足条件的 GPU 中, GPU1 恰恰是符合空闲资源满足条件但同时又是所剩资源最少的 GPU 卡,因此 GPU1 被选出。 3. 节点上运行 当 Pod 和节点绑定的事件被 Kubelet 接收到后,Kubelet 就会在节点上创建真正的 Pod 实体,在这个过程中, Kubelet 会调用 GPU Share Device Plugin 的 Allocate 方法, Allocate 方法的参数是 Pod 申请的 gpu-mem。而在 Allocate 方法中,会根据 GPU Share Scheduler Extender 的调度决策运行对应的 Pod 会列出该节点中所有状态为 Pending 并且 ALIYUN_COM_GPU_MEM_ASSIGNED 为 false 的 GPU Share Pod 选择出其中 Pod Annotation 的 ALIYUN_COM_GPU_MEM_POD 的数量与 Allocate 申请数量一致的 Pod。如果有多个符合这种条件的 Pod,就会选择其中 ALIYUN_COM_GPU_MEM_ASSUME_TIME 最早的 Pod。 将该 Pod 的 annotation ALIYUN_COM_GPU_MEM_ASSIGNED 设置为 true ,并且将 Pod annotation 中的 GPU 信息转化为环境变量返回给 Kubelet 用以真正的创建 Pod。 []( https://www.atatech.org/articles/132268#9) 相关项目 目前项目已经开源到 github.com 上 gpushare-scheduler-extender gpushare-device-plugin 部署 请参照 部署文档 []( https://www.atatech.org/articles/132268#11) 测试样例 首先创建一个使用 aliyun.com/gpu-mem 的应用 apiVersion: apps/v1 kind: Deployment metadata: name: binpack-1 labels: app: binpack-1 spec: replicas: 1 selector: # define how the deployment finds the pods it manages matchLabels: app: binpack-1 template: # define the pods specifications metadata: labels: app: binpack-1 spec: containers: - name: binpack-1 image: cheyang/gpu-player:v2 resources: limits: # MiB aliyun.com/gpu-mem: 1024 使用 请参照 使用文档 构建 请参照 如何构建 视频 Demo []( https://www.atatech.org/articles/132268#15)Demo 1: 部署多个 GPU Share 的 Pod,发现他们以 binpack 的方式被放置到同一个 GPU 卡上 []( https://www.atatech.org/articles/132268#16)Demo 2: 避免错误调度申请资源超过单个 GPU 可用资源的 Pod Roadmap 在 Device Plugin 中提供 Nvidia MPS 的可选支持; 支持该方案可以在由 kubeadm 初始化的 Kubernetes 集群自动化部署; 提升 Scheduler Extener 的高可用性; 为 GPU, RDMA 和弹性网卡提供通用方案。 作者: jessie筱姜 原文链接 本文为云栖社区原创内容,未经允许不得转载。
来源:OSCHINA
发布时间:2019-03-07 16:27:00
HDFS上传流程 客户端通过Distributed FileSystem模块向NameNode请求上传文件,NameNode检查目标文件是否已存在,父目录是否存在。 NameNode返回是否可以上传。 客户端请求第一个 block上传到哪几个datanode服务器上。 NameNode返回3个datanode节点,分别为dn1、dn2、dn3。 客户端通过FSDataOutputStream模块请求dn1上传数据,dn1收到请求会继续调用dn2,然后dn2调用dn3,将这个通信管道建立完成。 dn1、dn2、dn3逐级应答客户端。 客户端开始往dn1上传第一个block(先从磁盘读取数据放到一个本地内存缓存),以packet为单位,dn1收到一个packet就会传给dn2,dn2传给dn3;dn1每传一个packet会放入一个应答队列等待应答。 当一个block传输完成之后,客户端再次请求NameNode上传第二个block的服务器。(重复执行3-7步)。 HDFS读流程 客户端跟namenode通信,请求下载某个数据。 namenode查询元数据信息以及block位置信息。 将数据所在的datanode信息返回给客户端。 客户端根据数据所在的datanode,挑选一台距离自己最近的datanode,并向其发出下载文件的请求(若所需数据不在一台datanode上保存,则分别向多台datanode发出请求)。 datanode响应客户端请求,将数据返回给客户端。 从多个datanode获得的数据不断在客户端追加,形成完整的数据
来源:OSCHINA
发布时间:2020-03-26 12:01:00
本文是Choerodon 的微服务系列推文第五篇,上一篇《 Choerodon 的微服务之路(四):深入理解微服务配置中心 》介绍了配置中心在微服务架构中的作用,本篇将介绍微服务监控的重要性和必要性。 ▌文章的主要内容包括: 为什么要监控 开发者需要监控哪些 猪齿鱼的解决方案 在前面的几期的文章里,介绍了在 Choerodon 的微服务架构中,系统被拆分成多个有着独立部署能力的业务服务,每个服务可以使用不同的编程语言,不同的存储介质,来保持最低限度的集中式管理。 这样的架构决定了功能模块的部署是分布式的,不同的业务服务单独部署运行的,运行在独立的容器进程中,彼此之间通过网络进行服务调用交互。一次完整的业务流程会经过很多个微服务的处理和传递。 在这种情况下,如何监控服务的错误和异常,如何快速地定位和处理问题,以及如何在复杂的容器拓扑中筛选出用户所需要的指标,是 Choerodon 在监控中面临的首要问题。本文将会分享 Choerodon 在微服务下的监控思考,以及结合社区流行的 Spring Cloud、Kubernetes、Prometheus 等开源技术,打造的 Choerodon 的监控方案。 为什么要监控 在谈到 Choerodon 的监控之前,大家需要清楚为什么需要微服务下的监控。 传统的单体应用中,由于应用部署在具体的服务器上,开发者一般会从不同的层级对应用进行监控。比如猪齿鱼团队常用的方式是将监控分成基础设施、系统、应用、业务和用户端这几层,并对每一层分别进行监控。如下图所示。 而在微服务系统中,开发者对于监控的关心点是一样的,但是视角却发生了改变,从分层 + 机器的视角转化为以服务为中心的视角。在 Choerodon 中,传统的分层已经不太适用。服务是部署在 k8s 的 pod 中,而不是直接部署在服务器上。团队除了对服务器的监控之外,还需要考虑到 k8s 中容器的监控。同样,由于一个业务流程可能是通过一系列的业务服务而实现的,如何追踪业务流的处理也同样至关重要。 所以在微服务中,大家同样离不开监控,有效的监控能够帮开发者快速的定位故障,保护系统健康的运行。 平开发者需要监控哪些 在 Choerodon 中,将系统的使用人员分为应用的管理人员,开发人员,运维人员。而不同的人员在平台中所关心的问题则分别不同。 作为应用的管理人员,需要查看到系统中各个节点实例的运行状态以及实例中应用的状态。 作为开发人员,需要查看自己开发的服务在运行中的所有信息,也需要跟踪请求流的处理顺序和结果,并快速定位问题。 作为运维人员,需要查看系统集群中服务器的 CPU、内存、堆栈等信息。需要查看K8S集群的运行状态,同时也需要查看各个服务的运行日志。 除了这些以外,还需要监控到如下的一些信息: 服务的概览信息:服务的名称,相关的配置等基本信息。 服务的拓扑关系:服务之间的调用关系。 服务的调用链:服务之间的请求调用链。 服务的性能指标:服务的CPU,内存等。 接口的调用监控:接口的吞吐量,错误率,响应时间等。 服务的日志数据:服务运行中产生的日志异常。 简而概之,对于 Choerodon 而言,开发者将监控聚焦在指标监控,调用监控和日志监控。 猪齿鱼的解决方案 在开源社区中,有很多对监控的解决方案,比如指标监控有 Prometheus,链路监控有 zipkin、pinpoint,skywalking,日志则有 elk。 Choerodon 具有多集群多环境管理能力,Choerodon 为需要监控的集群配置监控组件,并与Choerodon 所在集群的监控组件互通以及过滤多余数据,可以最大限度地减少多集群非同一局域网的外网带宽需求。在多集群环境中仍然可以感知所管理应用的运行状态和配置预警信息。 ▌指标监控 Spring Boot 的执行器包含一系列的度量指标(Metrics)接口。当你请求 metrics 端点,你可能会看到类似以下的响应: { "counter.status.200.root": 20, "counter.status.200.metrics": 3, "counter.status.200.star-star": 5, "counter.status.401.root": 4, "gauge.response.star-star": 6, "gauge.response.root": 2, "gauge.response.metrics": 3, "classes": 5808, "classes.loaded": 5808, "classes.unloaded": 0, "heap": 3728384, "heap.committed": 986624, "heap.init": 262144, "heap.used": 52765, "nonheap": 0, "nonheap.committed": 77568, "nonheap.init": 2496, "nonheap.used": 75826, "mem": 986624, "mem.free": 933858, "processors": 8, "threads": 15, "threads.daemon": 11, "threads.peak": 15, "threads.totalStarted": 42, "uptime": 494836, "instance.uptime": 489782, "datasource.primary.active": 5, "datasource.primary.usage": 0.25 } 这些系统指标具体含义如下: 系统内存总量(mem),单位:KB 空闲内存数量(mem.free),单位:KB 处理器数量(processors) 系统正常运行时间(uptime),单位:毫秒 应用上下文(应用实例)正常运行时间(instance.uptime),单位:毫秒 系统平均负载(systemload.average) 堆信息(heap,heap.committed,heap.init,heap.used),单位:KB 线程信息(threads,thread.peak,thead.daemon) 类加载信息(classes,classes.loaded,classes.unloaded) 垃圾收集信息(gc.xxx.count, gc.xxx.time) 有了这些指标,我们只需要做简单的修改,就可以使这些指标被 Prometheus 所监测到。Prometheus 是一套开源的系统监控报警框架。默认情况下 Prometheus 暴露的metrics endpoint为/prometheus。 在项目的pom.xml文件中添加依赖,该依赖包含了 micrometer 和 prometheus 的依赖,并对监控的指标做了扩充。 io.choerodon choerodon-starter-hitoa ${choerodon.starters.version} Prometheus提供了4中不同的 Metrics 类型: Counter , Gauge , Histogram , Summary 。通过Gauge,Choerodon对程序的线程指标进行了扩充,添加了 NEW , RUNNABLE , BLOCKED , WAITING , TIMED_WAITING , TERMINATED 这几种类型,具体代码如下。 @Override public void bindTo(MeterRegistry registry) { Gauge.builder("jvm.thread.NEW.sum", threadStateBean, ThreadStateBean::getThreadStatusNEWCount) .tags(tags) .description("thread state NEW count") .register(registry); Gauge.builder("jvm.thread.RUNNABLE.sum", threadStateBean, ThreadStateBean::getThreadStatusRUNNABLECount) .tags(tags) .description("thread state RUNNABLE count") .register(registry); Gauge.builder("jvm.thread.BLOCKED.sum", threadStateBean, ThreadStateBean::getThreadStatusBLOCKEDCount) .tags(tags) .description("thread state BLOCKED count") .register(registry); Gauge.builder("jvm.thread.WAITING.sum", threadStateBean, ThreadStateBean::getThreadStatusWAITINGCount) .tags(tags) .description("thread state WAITING count") .register(registry); Gauge.builder("jvm.thread.TIMEDWAITING.sum", threadStateBean, ThreadStateBean::getThreadStatusTIMEDWAITINGCount) .tags(tags) .description("thread state TIMED_WAITING count") .register(registry); Gauge.builder("jvm.thread.TERMINATED.sum", threadStateBean, ThreadStateBean::getThreadStatusTERMINATEDCount) .tags(tags) .description("thread state TERMINATED count") .register(registry); } ▌调用监控 在微服务架构中,一个请求可能会涉及到多个服务,请求的路径则可能构成一个网状的调用链。而如果其中的某一个节点发生异常,则整个链条都可能受到影响。 针对这种情况,团队需要有一款调用链监控的工具,来支撑系统监控分布式的请求追踪。目前开源社区中有一些工具:Zipkin、Pinpoint、SkyWalking。Choerodon 使用的是 SkyWalking,它是一款国产的 APM 工具,包括了分布式追踪、性能指标分析、应用和服务依赖分析等。 Skywalking 包含 Agent 和 Collecter,具体的部署和原理在这里不在做具体的介绍,Choerodon 的服务在每个服务的 DockerFile 中,添加了对 Skywalking Agent 的支持。具体如下: FROM registry.cn-hangzhou.aliyuncs.com/choerodon-tools/javabase:0.7.1 COPY app.jar /iam-service.jar ENTRYPOINT exec java -XX:+UnlockExperimentalVMOptions -XX:+UseCGroupMemoryLimitForHeap $JAVA_OPTS $SKYWALKING_OPTS -jar /iam-service.jar 部署时通过配置容器的环境变量 SKYWALKING_OPTS 来实现客户端的配置。 ▌日志监控 日志是程序在运行中产生的遵循一定格式(通常包含时间戳)的文本数据,通常由Choerodon的服务生成,输出到不同的文件中,一般会有系统日志、应用日志、安全日志等等。这些日志分散地存储在不同的容器、机器中。当开发者在排查故障的时候,日志会帮助他们快速地定位到故障的原因。 Choerodon 采用了业界通用的日志数据管理解决方案,主要包括 elasticsearch 、 fluent-bit 、 fluentd 和 Kibana 。对于日志的采集分为如下几个步骤。 日志收集:通过 fluent-bit 读取 k8s 集群中 cluster 的日志,并进行收集。 日志过滤:通过 fluentd 将读取到的日志进行过滤,并进行缓存。 日志存储:将过滤后的日志存储至 elasticsearch 集群中。 日志展示:通过 kibana 查询 elasticsearch 中的日志数据,并用于展示。 通过端对端可视化的日志集中管理,给开发团队带来如下的一些好处: 故障查找:通过检索日志信息,定位相应的 bug ,找出解决方案。 服务分析:通过对日志信息进行统计、分析,了解服务器的负荷和服务运行状态。 数据分析:分析数据,对用户进行行为分析。 写在最后 回顾一下这篇文章,介绍了微服务监控的重要性和必要性,以及 Choerodon 是如何应对指标监控,调用监控和日志监控这三种监控的。微服务架构下的服务规模大,系统相对复杂,也使得众多开发者成为了微服务的受害者。如何做好微服务下的监控,保障系统健康地运行,我们仍有许多需要继续努力的。 更多关于微服务系列的文章,点击蓝字即可阅读 ▼ Choerodon的微服务之路(一):如何迈出关键的第一步 Choerodon的微服务之路(二):微服务网关 Choerodon 的微服务之路(三):服务注册与发现 Choerodon 的微服务之路(四):深入理解微服务配置中心 关于Choerodon猪齿鱼 Choerodon猪齿鱼 开源多云集成平台,基于开源技术Kubernetes,Istio,knative,Gitlab和Spring Cloud来实现本地和云端环境的集成,实现企业多云/混合云应用环境的一致性。平台通过提供精益敏捷、持续交付、容器环境、微服务、DevOps等能力来帮助组织团队来完成软件的生命周期管理,从而更快、更频繁地交付更稳定的软件。 大家也可以通过以下社区途径了解猪齿鱼的最新动态、产品特性,以及参与社区贡献: 官网: http://choerodon.io 论坛: http://forum.choerodon.io Github: https://github.com/choerodon 微信公众号:Choerodon猪齿鱼 微博:Choerodon猪齿鱼 欢迎加入Choerodon猪齿鱼社区,共同为企业数字化服务打造一个开放的生态平台。
来源:OSCHINA
发布时间:2019-03-25 19:47:00
速度快 ,因为数据都存于内存中,类似于HashMap,HashMap的优势就是查找和操作的时间复杂度都是O(1)。 支持丰富的数据类型 ,支持string、list、set、zset、hashmap等数据类型。 支持事务,操作都是原子性 ,即对数据的更改要么全部执行,要么全部不执行;R edis 对事务是部分支持的,如果是在入队时报错,那么都不会执行;在非入队时报错,那么成功的就会成功执行。 丰富的特性 , 可用作数据库、缓存和消息中间件,可以按 key 设置过期时间,过期后将会自动删除。
来源:OSCHINA
发布时间:2020-03-26 11:37:00
UCloud外网网关是为了承载外网IP、负载均衡等产品的外网出入向流量,当前基于Linux内核的OVS/GRE tunnel/netns/iptables等实现,很好地支撑了现有业务。同时,我们也在不断跟踪开源社区的新技术发展,并将之用于下一代外网网关的设计。这些新特性可将系统性能和管理能力再提上一档,满足未来几年的需求。在方案设计研发过程中发现,新特性存在不少缺陷和Bug,为此我们向开源社区回馈了10多个patch,并融入到kernel 5.0版本中,帮助完善kernel功能并提升稳定性。 当前业界的多租户外网网关很多都是基于OpenFlow的OpenvSwitch(OVS)方案,然而随着内核路由转发功能的不断完善,利用内核原生路由转发方式进行设计多租户外网网关系统成为一种可能。在这种方式下能有效的使用传统iproute2路由工具以及iptables、nftables等Firewall工具,并且随着SwitchDev技术的兴起,未来将网关系统迁移到Linux Switch上也成为一种可能。 现有kernel 3.x的不足 当前广泛使用的内核版本为3.x系列,例如CentOS 7全系列标准支持的内核为3.10版本,Fedora/Ubuntu等Linux发行版也有大量使用。在3.x系列内核下存在着IP tunnel管理复杂、租户隔离性能损耗等问题。 1. IP tunnel管理复杂 Linux内核创建IP tunnel设备来建立点对点的隧道连接,创建时需指定tunnel dst和 tunnel key。因为宿主机之间两两建立连接,面向宿主机的目的地址众多,这样就会导致网关节点上需要创建成千上万的tunnel设备,在大规模业务环境下,tunnel的管理将变得及其复杂。 2. 多租户隔离导致的性能下降 a. 公有云需要实现多租户隔离以确保用户间的安全和隐私。由于VPC网络下不同租户的内网地址可以重合,导致路由也有重合的可能性,此时需要通过大量的策略路由去隔离租户的路由规则,由于策略路由的链表属性,性能会随着链表长度的增加而急剧下降。 b. 由于Firewall和NAT的实现基于同样链式的iptables,性能损耗同样可观。 3. netns带来性能开销 通过netns实现租户路由和Firewall规则的隔离,但是netns会引入虚拟网卡和协议栈重入开销,使整体性能下降20%左右。 三项内核新技术 为了解决原有方案存在的困扰,我们调研了大量行业主流方案和内核上游的新动向,发现Lightweight tunneling(轻量级隧道,简称lwtunnel)、Virtual Routing Forwarding(虚拟路由转发,简称VRF)以及nftable & netfilter flow offload(流卸载)三项内核新技术的特性,可以帮助规避原方案存在的缺陷。 1. Lightweight tunneling Linux内核在4.3版本中引入了轻量级隧道Lightweight tunneling,它提供了通过route方式设置tunnel属性的方法,这样可以避免管理大量的tunnel设备。 创建隧道设备时指定external模式,利用路由设置的轻量级隧道通过tun设备发送报文。 2. Virtual Routing Forwarding Linux内核在4.3版本中引入了VRF的初步支持,并在4.8版本形成完备版本。Virtual Routing Forwarding虚拟路由转发,可以将一台Linux Box的物理路由器当多台虚拟路由器使用,能很好的解决租户路由隔离问题,避免直接使用策略路由。因此,可以将不同租户的网卡加入租户所属的虚拟路由器中来实现多租户的虚拟路由。 3. flow offload Nftables是一种新的数据包分类框架,旨在替代现存的{ip,ip6,arp,eb}_tables。在nftables中,大部分工作是在用户态完成的,内核只知道一些基本指令(过滤是用伪状态机实现的)。nftables的一个高级特性就是映射,可以使用不同类型的数据并映射它们。例如,我们可以映射iif device到专用的规则集合(之前创建的存储在一个链中)。由于是hash映射的方式,可以完美的避免链式规则跳转的性能开销。 Linux内核在版本4.16引入了flow offload功能,它为IP forward提供了基于流的卸载功能。当一条新建连接完成首回合原方向和反方向的报文时,完成路由,Firewall和NAT工作后,在处理反方向首报文的forward hook,根据报文路由、NAT等信息创建可卸载flow到接收网卡ingress hook上。后续的报文可以在接收ingress hook上直接转发,不需要再进入IP stack处理。此外,将来flow offload还将支持hardware offload模式,这将极大提高系统转发性能。 方案设计与优化实践 通过对上述三项新技术的研究,我们发现可以尝试设计一套基于路由的方式,实现多租户overlay网络的外网网关。在方案设计过程中,我们也碰到了诸如lwtunnel和flow offload功能不足,以及VRF和flow offload不能一起有效的工作等问题。最终我们都设法解决了,并针对这些内核的不足提交patch给Linux开源社区。 1. lwtunnel发送报文tunnel_key丢失 **问题描述:**我们利用lwtunnel路由方式发送报文时,创建了一个external类型的gretap tunnel,我们将命令设置了id为1000,但是发送成功报文中没有tunnel_key字段。 **问题定位:**我们研究iproute2代码,发现由于TUNNEL_KEY flag并没有开放给用户态,所以iproute2工具并没有对lwtunnel路由设置TUNNEL_KEY,导致报文不会创建tunnel_key字段。 提交patch: 我们给内核和用户态iproute2分别提交patch来解决这一问题: iptunnel: make TUNNEL_FLAGS available in uapi https://git.kernel.org/pub/scm/linux/kernel/git/davem/net-next.git/commit/ ? id=1875a9ab01dfa96b06cb6649cb1ce56efa86c7cb iproute: Set ip/ip6 lwtunnel flags https://git.kernel.org/pub/scm/network/iproute2/iproute2.git/commit/?id=3d65cefbefc86a53877f1e6461a9461e5b8fd7b3 提交patch后,可以通过以下方式设置路由。 ip r r 2.2.2.11 via 1.1.1.11 dev tun encap ip id 1000 dst 172.168.0.1 key 2. lwtunnel对指定key的IP tunnel无效 **问题发现:**为了能有效隔离租户路由,我们给每个租户创建一个基于tunnel_key的gretap tunnel设备。如下图,创建一个tunnel_key 1000的gretap tunnel设备,把tunnel设备加入租户所属VRF,tunnel设备能有效地接收报文,但并不能发送报文。 问题定位:研究内核发现,IP tunnel在非external模式下即使指定了轻量级隧道路由,发送报文也没有使用它,导致报文路由错误被丢弃。 提交patch: ip_tunnel: Make none-tunnel-dst tunnel port work with lwtunnel https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=d71b57532d70c03f4671dd04e84157ac6bf021b0 提交patch后,在未指定tunnel_dst的非external模式IP tunnel下,能使用轻量级隧道路由进行发送报文。 3. external IP tunnel ARP无法正常运行 **问题描述:**邻居IP tunnel进行了ARP请求,但是本端的ARP回应报文的隧道头中并没带tunnel_key字段。 **问题定位:**研究代码发现,tunnel收到了对端的ARP 请求,在发送报文ARP回复的时候会复制请求报文的tunnel信息,但是遗漏了所有tun_flags。 提交patch: iptunnel: Set tun_flags in the iptunnel_metadata_reply from src https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=7bdca378b2301b1fc6a95c60d6d428408ae4e39e 4. Flow offload不能与DNAT有效工作 **问题描述:**Firewall创建规则从eth0收到目的地址2.2.2.11的报文,DNAT为10.0.0.7, flow offload无法工作。 **问题定位:**分析发现,客户端1.1.1.7 —> 2.2.2.7 DNAT到server 10.0.0.7,第一个reply反向报文(syc+ack)使用了错的目的地址获取反向路由 daddr = ct->tuplehash[!dir].tuple.dst.u3.ip 此时dir为反方向,所以daddr获取为原方向的目的地址,这个值是2.2.2.7, 但是由于被DNAT过,真正的路由不应该通过2.2.2.7去获取,而是应该根据10.0.0.7这个值去获取 addr = ct->tuplehash[dir].tuple.src.u3.ip 提交patch: netfilter: nft_flow_offload: Fix reverse route lookup https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=a799aea0988ea0d1b1f263e996fdad2f6133c680 5. Flow offload不能与VRF有效工作 问题描述: 将网卡eth0和eth1加入VFR后,flow offload不起作用。 **问题定位:**查看代码发现,原方向和反方向首报文进入协议堆栈后skb->dev会设置为vrf device user1,创建flow offload规则的iif就是user1。但是offload规则下发在eth0和eth1的ingress hook上,所以后续报文在eth0和eth1的ingress hook上不能匹配flow规则。 提交patch: netfilter: nft_flow_offload: fix interaction with vrf slave device https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=10f4e765879e514e1ce7f52ed26603047af196e2 最终,我们根据两个方向查找路由的结果,设置flow offload规则的iif和oif信息来解决此问题。 6. VRF PREROUTING hook重入问题 **问题描述:**配置网卡加入VRF,firewall ingress方向规则为接收目的地址2.2.2.11 、TCP 目的端口22的报文,egress方向规则为丢弃TCP 目的端口 22的报文。出现异常结果: 收到目的地址2.2.2.11 TCP 22目的端口的报文却被丢弃。 **问题定位:**研究发现网卡加入VRF后收到的报文会两次进入PREROUTING hook,因为在进入IP stack时会进第一次PREROUTING hook,然后被VRF设备接管后会再次进入PREROUTING hook。上述规则第一次在rule-1000-ingress chain中dst nat为10.0.0.7,第二次由于报文被DNAT后会错误的进入rule-1000-egress,导致报文被丢弃。 **提交patch:**我们给内核加了一个支持判断网卡类型的match项目,让用户态避免可知的第二次无效重入,内核态和用户态nftables分别提交了如下的patch: netfilter: nft_meta: Add NFT_META_I/OIFKIND meta type https://git.kernel.org/pub/scm/linux/kernel/git/davem/net-next.git/commit/?id=0fb4d21956f4a9af225594a46857ccf29bd747bc meta: add iifkind and oifkind support http://git.netfilter.org/nftables/commit/?id=512795a673f999fb04b84dbbbe41174e9c581430 使用方法: nft add rule firewall rules-all meta iifkind "vrf" counter accept 原型验证 最终,我们成功地利用lwtunnel、VRF和flow offload实现多租户外网网关的原型验证。验证过程如下: 1. 首先创建原型环境。 a. netns cl模拟外网client, 地址为1.1.1.7,tunnel src 172.168.0.7,配置发送路由; b. netns ns1模拟租户1,内网地址为10.0.0.7,外网地址为 2.2.2.11,tunnel src 172.168.0.11 tunnel_key 1000,配置发送路由; c. netns ns2模拟租户2,内网地址为10.0.0.7,外网地址为 2.2.2.12,tunnel src 172.168.0.12 tunnel_key 2000,配置发送路由; d. Host模拟外网网关,tunnel src 172.168.0.1,创建租户VRF user1和use2,创建租户IP tunnel tun1和tun2,配置转发路由。 原型环境图如下: 2. 创建firewall规则: a. 租户1入向允许TCP目的端口22和ICMP访问,出向禁止访问外部TCP 22目的端口; b. 租户2入向允许TCP端口23和ICMP访问,出向禁止访问外部TCP 23目的端口; c. 在租户tun1和tun2设备上支持flow offload。 最终,client可以通过2.2.2.11成功访问user1 tcp 22端口服务,user1不能访问client tcp 22端口服务;client可以通过2.2.2.12成功访问user2 tcp 23端口服务,user1不能访问client tcp 23端口服务。 待后续hardware offload功能完善以及网卡厂商支持后,我们会做进一步的开发验证。 写在最后 以上是本项目涉及的部分核心问题,这些patch特性都可以在Linux kernel 5.0版本里获取。我们把这期间为Linux kernel社区贡献的patch整理成了一份列表,希望能为开发者提供帮助,读者可以点击 “阅读原文” 阅览完整patch list。 Linux作为成熟的开源套件,一直是云厂商使用的主流操作系统,但在技术的更新迭代过程中,一些新特性在实际应用上也会存在稳定性、兼容性等方面的问题。我们在研究使用上游技术的同时,也一直积极探索、丰富开源技术功能,帮助提高开源技术稳定性。并将产出持续回馈给社区,与社区共同构建一个繁荣的开源生态。
来源:OSCHINA
发布时间:2019-03-25 17:57:00
如果master异常,则会进行master-slave切换,将其中一个slave作为master,将之前的master作为slave。 哨兵作用 哨兵是Redis集群架构中非常重要的一个组件,主要功能如下: 集群监控:负责监控redis master和slave进程是否正常 消息通知:如果某个redis实例有故障,那么哨兵负责发送消息作为报警通知给管理员 故障转移:如果master节点挂掉了,会自动转移到slave节点上 配置中心:如果故障转移发生了,通知client客户端新的master地址 哨兵的核心知识 故障转移时,判断一个master节点是否宕机了,需要大部分的哨兵都同意才行,涉及到了分布式选举的问题 哨兵至少需要3个实例,来保证自己的健壮性 哨兵+redis主从的部署架构,是不会保证数据零丢失的,只能保证redis集群的高可用性 sdown和odown sdown和odown两种失败的状态 sdown是主观宕机,就一个哨兵如果自己觉得一个master宕机了,那么就是主观宕机 odown是客观宕机,如果quorum数量的哨兵都觉得一个master宕机了,那么就是客观宕机 sdown达成的条件:如果一个哨兵ping一个master,超过了is-master-down-after-milliseconds指定的毫秒数之后,就认为master宕机 odown达成条件:如果一个哨兵在指定的时间内,收到了quorum指定数量的其他哨兵也认为那个master是宕机了,那么就认为是odown了,客观认为master宕机了 quorum和majority quorum:确认odown的最少哨兵数量 majority:授权进行主从切换的最少哨兵数量 每一个哨兵要做主备切换,首先需要quorum数量的哨兵认为odown,然后选举出一个哨兵来做切换,这个哨兵还得得到majority哨兵的特权,才能进行切换。 如果quorummajority,那么必须quorum数量的哨兵都授权,比如5个哨兵,quorum是5,那么必须5个哨兵都同意授权才能执行。(谁多听谁的) 为什么哨兵至少3个节点? 哨兵集群必须部署两个以上节点。如果哨兵集群仅仅部署了2个哨兵实例,那么它的majority就是2(2的majority=2,3的majority=2,5的majority=3,4的majority=2),如果其中一个哨兵宕机了,就无法满足majority>=2这个条件,那么master发生故障时也就无法进行主从切换了。 工作原理 每个Sentienl以每秒钟一次的频率向他所知的Master,Slave以及其他的Sentinel实例发送一个ping命令 如果一个实例距离最后一次有效回复ping命令的时间超过了down-after-milliseconds选项所指的值,则这个实例会被Sentinel标记为主观宕机 如果一个master被标记为主观宕机,则正在监视这个master的所有sentinel要以每一秒一次的频率确认Master的确进入了主观宕机状态 当有足够数量的Sentinel(大于等于配置文件所指的值)在指定的时间范围内确认master的确进入了主观宕机状态,则master会被标记为客观状态 在一般情况下,每个Sentinel会以1次/10秒的频率向他一致的所有master,slave发送INFO命令 当master被Sentinel标记为客观宕机是,Sentinel向下线的master的所有slave发送INFO命令的频率会从1次/10秒改为1次/秒 若没有足够数量的Sentinel同意master已经下线,master的客观宕机状态就会被移除;若master重新想Sentinel的ping命令返回有效回复,master的主观宕机状态就会被移除。 哨兵模式的配置 首先 配置redis的主从服务器 ,修改redis.conf文件如下 # 使得Redis服务器可以跨网络访问 bind 0.0.0.0 # 设置密码 requirepass "123456" # 指定主服务器,注意:有关slaveof的配置只是配置从服务器,主服务器不需要配置 slaveof 192.168.11.128 6379 # 主服务器密码,注意:有关slaveof的配置只是配置从服务器,主服务器不需要配置 masterauth 123456 上述内容主要是配置Redis服务器,从服务器比主服务器多了一个slaveof的配置和密码 配置3个哨兵 ,每个哨兵都是一样的。在Redis安装目录下有一个sentinel.conf文件,copy一份进行修改 # 禁止保护模式 protected-mode no # 配置监听的主服务器,这里sentinel monitor代表监控,mymaster代表服务器的名称,可以自定义,192.168.11.128代表监控的主服务器,6379代表端口,2代表只有两个或两个以上的哨兵认为主服务器不可用的时候,才会进行failover操作。 sentinel monitor mymaster 192.168.11.128 6379 2 # sentinel author-pass定义服务的密码,mymaster是服务名称,123456是Redis服务器密码 # sentinel auth-pass sentinel auth-pass mymaster 123456 启动服务器和哨兵 ,进入Redis安装目录的src目录 # 启动Redis服务器进程 ./redis-server ../redis.conf # 启动哨兵进程 ./redis-sentinel ../sentinel.conf 注意启动顺序: 首先是主机(192.168.11.128)的Redis服务进程,然后启动丛机的服务进程,最后启动3个哨兵的服务进程 Java中使用哨兵模式 /** * 测试Redis哨兵模式 * @author liu */ public class TestSentinels { @SuppressWarnings("resource") @Test public void testSentinel() { JedisPoolConfig jedisPoolConfig = new JedisPoolConfig(); jedisPoolConfig.setMaxTotal(10); jedisPoolConfig.setMaxIdle(5); jedisPoolConfig.setMinIdle(5); // 哨兵信息 Set sentinels = new HashSet<>(Arrays.asList("192.168.11.128:26379", "192.168.11.129:26379","192.168.11.130:26379")); // 创建连接池 JedisSentinelPool pool = new JedisSentinelPool("mymaster", sentinels,jedisPoolConfig,"123456"); // 获取客户端 Jedis jedis = pool.getResource(); // 执行两个命令 jedis.set("mykey", "myvalue"); String value = jedis.get("mykey"); System.out.println(value); } }
来源:OSCHINA
发布时间:2020-03-26 11:24:00
MySql的镜像,默认情况下,MySql5.7中的sql_mode含有only_full_group_by,group by语句有时候会报错。通过手动修改sql_mode,那么如果删除容器或者新建容器,就会导致我们手动设置的sql_mode失效,所以自己制作一个基于mysql镜像的镜像,解决sql_mode含有only_full_group_by的问题。 运行容器 List-1 mjduan@mjduan:/opt % docker run -d -p 3306:3306 -e MYSQL_ALLOW_EMPTY_PASSWORD --name mysql1 mysql:5.7.9 5514c31a4e0bc524cee3cdcb962ac73b4fdeb1b5b32d70fa2840e9029b203a8c 进入容器安装vim List-2 mjduan@mjduan:/opt % docker exec -ti mysql1 /bin/bash #在容器内执行如下命令 root@44504961189a:/opt % apt-get update ...... root@44504961189a:/opt % apt-get install vim ...... 在容器内,/etc/mysql/conf.d/下,新建.cnf文件,将配置写入到里面,我们新建custom.cnf文件,写入如下内容: List-3 root@44504961189a:/# more /etc/mysql/conf.d/custom.cnf [mysqld] sql_mode=STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_AUTO_CREATE_USER,NO_ENGINE_SUBSTITUTION 退出容器,用docker commit命令由容器制作镜像, List-4 mjduan@mjduan:/opt % docker commit mysql1 mysql_custom:1.0 之后docker images就可以看到镜像mysql_custom:1.0了。 mysql_custom:1.0就是我们需要的,用它来启动容器,如下List-5 List-5 mjduan@mjduan:/opt % docker run -d -p 3306:3306 -e MYSQL_ROOT_PASSWORD=新密码 --name mysql_mjduan2 mysql_custom:1.0 44504961189a45442a6b33e5945778b73bc3dd058ab9e794c56b0bbfc3e603bf 之后进入容器mysql_mjduan2,用mysql命令进入mysql命令行时就会提示要root密码了。再次查看sql_mode,就会看到sql_mode没有only_full_group_by了。 注意,不要让别人拿到你的镜像,否则通过docker inspect命令就可以看到你设置的root密码。不过可以在创建容器的时候用MYSQL_ROOT_PASSWORD来设置新的root密码。 注: 也可以直接用Dockerfile基于mysql的镜像进行制作。
来源:OSCHINA
发布时间:2019-03-24 20:48:00
Redis不仅支持简单的key-value数据类型,同时还提供string、list、set、zset、hash等数据结构的存储;而Memcached仅仅支持简单的key-value数据类型。 Redis支持数据的持久化,可以将内存中的数据保存到磁盘中,重启的时候再次加载使用;而Memcached将数据全部存于内存中。 Redis支持数据备份,即master-slave模式的数据备份。 Redis比Memcached的(读写)速度要快很多。 Redis使用的是单线程IO复用模型;而Memcached使用的是多线程非阻塞IO复用模型。
来源:OSCHINA
发布时间:2020-03-26 11:24:00
RadosClient.h class librados::RadosClient : public Dispatcher //继承自Dispatcher(消息分发类) { public: using Dispatcher::cct; md_config_t *conf; //配置文件 private: enum { DISCONNECTED, CONNECTING, CONNECTED, } state; //网络连接状态 MonClient monclient; // monc Messenger *messenger; //网络消息接口 uint64_t instance_id; //相关消息分发 Dispatcher类的函数重写 bool _dispatch(Message *m); bool ms_dispatch(Message *m); bool ms_get_authorizer(int dest_type, AuthAuthorizer **authorizer, bool force_new); void ms_handle_connect(Connection *con); bool ms_handle_reset(Connection *con); void ms_handle_remote_reset(Connection *con); Objecter *objecter; // Osdc模块中的 用于发送封装好的OP消息 Mutex lock; Cond cond; SafeTimer timer; //定时器 int refcnt; version_t log_last_version; rados_log_callback_t log_cb; void *log_cb_arg; string log_watch; int wait_for_osdmap(); public: Finisher finisher; // 回调函数的类 explicit RadosClient(CephContext *cct_); ~RadosClient(); int ping_monitor(string mon_id, string *result); int connect(); // 连接 void shutdown(); int watch_flush(); int async_watch_flush(AioCompletionImpl *c); uint64_t get_instance_id(); int wait_for_latest_osdmap(); // 根据pool名字或id创建ioctx int create_ioctx(const char *name, IoCtxImpl **io); int create_ioctx(int64_t, IoCtxImpl **io); int get_fsid(std::string *s); // pool相关操作 int64_t lookup_pool(const char *name); bool pool_requires_alignment(int64_t pool_id); int pool_requires_alignment2(int64_t pool_id, bool *requires); uint64_t pool_required_alignment(int64_t pool_id); int pool_required_alignment2(int64_t pool_id, uint64_t *alignment); int pool_get_auid(uint64_t pool_id, unsigned long long *auid); int pool_get_name(uint64_t pool_id, std::string *auid); int pool_list(std::list >& ls); int get_pool_stats(std::list& ls, map& result); int get_fs_stats(ceph_statfs& result); /* -1 was set as the default value and monitor will pickup the right crush rule with below order: a) osd pool default crush replicated ruleset b) the first ruleset in crush ruleset c) error out if no value find */ // 同步创建pool 和 异步创建pool int pool_create(string& name, unsigned long long auid=0, int16_t crush_rule=-1); int pool_create_async(string& name, PoolAsyncCompletionImpl *c, unsigned long long auid=0, int16_t crush_rule=-1); int pool_get_base_tier(int64_t pool_id, int64_t* base_tier); //同步删除和异步删除 int pool_delete(const char *name); int pool_delete_async(const char *name, PoolAsyncCompletionImpl *c); int blacklist_add(const string& client_address, uint32_t expire_seconds); //Mon命令处理,调用monclient.start_mon_command 把命令发送给Mon处理 int mon_command(const vector& cmd, const bufferlist &inbl, bufferlist *outbl, string *outs); int mon_command(int rank, const vector& cmd, const bufferlist &inbl, bufferlist *outbl, string *outs); int mon_command(string name, const vector& cmd, const bufferlist &inbl, bufferlist *outbl, string *outs); //OSD命令处理,objector->osd_command 把命令发送给OSD处理 int osd_command(int osd, vector& cmd, const bufferlist& inbl, bufferlist *poutbl, string *prs); //PG命令处理,objector->pg_command 把命令发送给PG处理 int pg_command(pg_t pgid, vector& cmd, const bufferlist& inbl, bufferlist *poutbl, string *prs); void handle_log(MLog *m); int monitor_log(const string& level, rados_log_callback_t cb, void *arg); void get(); bool put(); void blacklist_self(bool set); }; connect() 连接 int librados::RadosClient::connect() { common_init_finish(cct); int err; // already connected? if (state == CONNECTING) return -EINPROGRESS; if (state == CONNECTED) return -EISCONN; state = CONNECTING; // get monmap err = monclient.build_initial_monmap(); //通过配置文件获取初始化的Monitor if (err < 0) goto out; err = -ENOMEM; messenger = Messenger::create_client_messenger(cct, "radosclient"); //创建通信模块 if (!messenger) goto out; // require OSDREPLYMUX feature. this means we will fail to talk to // old servers. this is necessary because otherwise we won't know // how to decompose the reply data into its consituent pieces. messenger->set_default_policy(Messenger::Policy::lossy_client(0, CEPH_FEATURE_OSDREPLYMUX)); ldout(cct, 1) << "starting msgr at " << messenger->get_myaddr() << dendl; ldout(cct, 1) << "starting objecter" << dendl; //创建objecter objecter = new (std::nothrow) Objecter(cct, messenger, &monclient, &finisher, cct->_conf->rados_mon_op_timeout, cct->_conf->rados_osd_op_timeout); if (!objecter) goto out; objecter->set_balanced_budget(); // mc添加 messenger monclient.set_messenger(messenger); // objecter 初始化 objecter->init(); // messenger添加 dispather messenger->add_dispatcher_tail(objecter); messenger->add_dispatcher_tail(this); // messenger启动 messenger->start(); ldout(cct, 1) << "setting wanted keys" << dendl; monclient.set_want_keys(CEPH_ENTITY_TYPE_MON | CEPH_ENTITY_TYPE_OSD); ldout(cct, 1) << "calling monclient init" << dendl; // mc 初始化 err = monclient.init(); if (err) { ldout(cct, 0) << conf->name << " initialization error " << cpp_strerror(-err) << dendl; shutdown(); goto out; } err = monclient.authenticate(conf->client_mount_timeout); if (err) { ldout(cct, 0) << conf->name << " authentication error " << cpp_strerror(-err) << dendl; shutdown(); goto out; } messenger->set_myname(entity_name_t::CLIENT(monclient.get_global_id())); objecter->set_client_incarnation(0); // objecter 启动 objecter->start(); lock.Lock(); // 定时器初始化 timer.init(); monclient.renew_subs(); //执行回调的完成类start finisher.start(); // 更新 状态为已连接 state = CONNECTED; instance_id = monclient.get_global_id(); ... } create_ioctx 根据pool创建ioctx int librados::RadosClient::create_ioctx(const char *name, IoCtxImpl **io) { // 获取 poolid int64_t poolid = lookup_pool(name); ... // 创建 IoCtxImpl *io = new librados::IoCtxImpl(this, objecter, poolid, CEPH_NOSNAP); return 0; } Mon OSD pg 命令操作 int librados::RadosClient::mon_command(const vector& cmd, const bufferlist &inbl, bufferlist *outbl, string *outs) { // mc start_mon_command 发送到monitor monclient.start_mon_command(cmd, inbl, outbl, outs, new C_SafeCond(&mylock, &cond, &done, &rval)); } int librados::RadosClient::osd_command(int osd, vector& cmd, const bufferlist& inbl, bufferlist *poutbl, string *prs) { // 发送到osd int r = objecter->osd_command(osd, cmd, inbl, &tid, poutbl, prs, new C_SafeCond(&mylock, &cond, &done, &ret)); } int librados::RadosClient::pg_command(pg_t pgid, vector& cmd, const bufferlist& inbl, bufferlist *poutbl, string *prs) { // 发送到pg int r = objecter->pg_command(pgid, cmd, inbl, &tid, poutbl, prs, new C_SafeCond(&mylock, &cond, &done, &ret)); } Ioctximpl librados::IoCtx的实现IoCtxImpl 把请求封装成ObjectOperation 类(osdc 中的) 把相关的pool信息添加到里面,封装成Objecter::Op对像 调用相应的函数 objecter- >op_submit 发送给相应的OSD 操作完成后,调用相应的回调函数。 如rados_write extern "C" int rados_write(rados_ioctx_t io, const char *o, const char *buf, size_t len, uint64_t off) { librados::IoCtxImpl *ctx = (librados::IoCtxImpl *)io; object_t oid(o); bufferlist bl; bl.append(buf, len); int retval = ctx->write(oid, bl, len, off); } 调用IoCtxImpl::write int librados::IoCtxImpl::write(const object_t& oid, bufferlist& bl, size_t len, uint64_t off) { ::ObjectOperation op; prepare_assert_ops(&op); // assert ops bufferlist mybl; mybl.substr_of(bl, 0, len); op.write(off, mybl); // 封装到op.write Objecter.h ObjectOperation write return operate(oid, &op, NULL); // IoCtxImpl::operate } int librados::IoCtxImpl::operate(const object_t& oid, ::ObjectOperation *o, ceph::real_time *pmtime, int flags) { int op = o->ops[0].op.op; Objecter::Op *objecter_op = objecter->prepare_mutate_op(oid, oloc, *o, snapc, ut, flags, NULL, oncommit, &ver); objecter->op_submit(objecter_op); } AioCompletionImpl OSDC ObjectOperation struct ObjectOperation { vector ops; // ops集合 int flags; int priority; vector out_bl; // 输出bufferlist vector out_handler; // 回调函数 vector out_rval; // 返回码集合 size_t size() { // op个数 return ops.size(); } /** * This is a more limited form of C_Contexts, but that requires * a ceph_context which we don't have here. */ // 用户添加回调函数 class C_TwoContexts : public Context { Context *first; Context *second; }; /** * Add a callback to run when this operation completes, * after any other callbacks for it. */ // 添加回调函数 void add_handler(Context *extra) { size_t last = out_handler.size() - 1; Context *orig = out_handler[last]; if (orig) { Context *wrapper = new C_TwoContexts(orig, extra); out_handler[last] = wrapper; } else { out_handler[last] = extra; } } // 添加操作 OSDOp& add_op(int op) { int s = ops.size(); ops.resize(s+1); ops[s].op.op = op; out_bl.resize(s+1); out_bl[s] = NULL; out_handler.resize(s+1); out_handler[s] = NULL; out_rval.resize(s+1); out_rval[s] = NULL; return ops[s]; } // 添加data void add_data(int op, uint64_t off, uint64_t len, bufferlist& bl) { OSDOp& osd_op = add_op(op); osd_op.op.extent.offset = off; osd_op.op.extent.length = len; osd_op.indata.claim_append(bl); } void add_clone_range(int op, uint64_t off, uint64_t len, const object_t& srcoid, uint64_t srcoff, snapid_t srcsnapid) {} void add_xattr(int op, const char *name, const bufferlist& data) {} void add_xattr_cmp(int op, const char *name, uint8_t cmp_op, uint8_t cmp_mode, const bufferlist& data) {} // 添加call method void add_call(int op, const char *cname, const char *method, bufferlist &indata, bufferlist *outbl, Context *ctx, int *prval) { OSDOp& osd_op = add_op(op); unsigned p = ops.size() - 1; out_handler[p] = ctx; out_bl[p] = outbl; out_rval[p] = prval; osd_op.op.cls.class_len = strlen(cname); osd_op.op.cls.method_len = strlen(method); osd_op.op.cls.indata_len = indata.length(); osd_op.indata.append(cname, osd_op.op.cls.class_len); osd_op.indata.append(method, osd_op.op.cls.method_len); osd_op.indata.append(indata); } void add_pgls(int op, uint64_t count, collection_list_handle_t cookie, epoch_t start_epoch) {} void add_pgls_filter(int op, uint64_t count, const bufferlist& filter, collection_list_handle_t cookie, epoch_t start_epoch) {} void add_alloc_hint(int op, uint64_t expected_object_size, uint64_t expected_write_size) {} // ------ // pg 操作 void pg_ls(uint64_t count, bufferlist& filter, collection_list_handle_t cookie, epoch_t start_epoch) {} void pg_nls(uint64_t count, const bufferlist& filter, collection_list_handle_t cookie, epoch_t start_epoch) {} // 创建 操作 void create(bool excl) { OSDOp& o = add_op(CEPH_OSD_OP_CREATE); o.op.flags = (excl ? CEPH_OSD_OP_FLAG_EXCL : 0); } // 状态 struct C_ObjectOperation_stat : public Context { bufferlist bl; uint64_t *psize; ceph::real_time *pmtime; time_t *ptime; struct timespec *pts; int *prval; // 完成大小,时间等 void finish(int r) {} } }; // 查看状态,获取C_ObjectOperation_stat void stat(uint64_t *psize, ceph::real_time *pmtime, int *prval) {} void stat(uint64_t *psize, time_t *ptime, int *prval) {} void stat(uint64_t *psize, struct timespec *pts, int *prval) {} // object data // 读操作 void read(uint64_t off, uint64_t len, bufferlist *pbl, int *prval, Context* ctx) { bufferlist bl; add_data(CEPH_OSD_OP_READ, off, len, bl); unsigned p = ops.size() - 1; out_bl[p] = pbl; out_rval[p] = prval; out_handler[p] = ctx; } void sparse_read(uint64_t off, uint64_t len, std::map *m, bufferlist *data_bl, int *prval) {} // 写操作 void write(uint64_t off, bufferlist& bl, uint64_t truncate_size, uint32_t truncate_seq) { add_data(CEPH_OSD_OP_WRITE, off, bl.length(), bl); // 添加data, 将WRITE存入ops,将数据放在op中 OSDOp& o = *ops.rbegin(); o.op.extent.truncate_size = truncate_size; o.op.extent.truncate_seq = truncate_seq; } void write(uint64_t off, bufferlist& bl) {} void write_full(bufferlist& bl) {} void append(bufferlist& bl) {} void zero(uint64_t off, uint64_t len) {} void truncate(uint64_t off) {} void remove() {} void mapext(uint64_t off, uint64_t len) {} void sparse_read(uint64_t off, uint64_t len) {} void clone_range(const object_t& src_oid, uint64_t src_offset, uint64_t len, uint64_t dst_offset) {} // object attrs // 属性操作 void getxattr(const char *name, bufferlist *pbl, int *prval) {} void getxattrs(std::map *pattrs, int *prval) {} void setxattr(const char *name, const bufferlist& bl) {} void setxattr(const char *name, const string& s) {} void cmpxattr(const char *name, uint8_t cmp_op, uint8_t cmp_mode, const bufferlist& bl) {} void rmxattr(const char *name) {} void setxattrs(map& attrs) {} void resetxattrs(const char *prefix, map& attrs) {} // trivialmap void tmap_update(bufferlist& bl) {} void tmap_put(bufferlist& bl) {} void tmap_get(bufferlist *pbl, int *prval) {} void tmap_get() {} void tmap_to_omap(bool nullok=false) {} // objectmap void omap_get_keys(const string &start_after, uint64_t max_to_get, std::set *out_set, int *prval) { OSDOp &op = add_op(CEPH_OSD_OP_OMAPGETKEYS); bufferlist bl; ::encode(start_after, bl); ::encode(max_to_get, bl); op.op.extent.offset = 0; op.op.extent.length = bl.length(); op.indata.claim_append(bl); if (prval || out_set) { unsigned p = ops.size() - 1; C_ObjectOperation_decodekeys *h = new C_ObjectOperation_decodekeys(out_set, prval); out_handler[p] = h; out_bl[p] = &h->bl; out_rval[p] = prval; } } void omap_get_vals(const string &start_after, const string &filter_prefix, uint64_t max_to_get, std::map *out_set, int *prval) {} void omap_get_vals_by_keys(const std::set &to_get, std::map *out_set, int *prval) {} void omap_cmp(const std::map > &assertions, int *prval) {} void copy_get(object_copy_cursor_t *cursor, uint64_t max, uint64_t *out_size, ceph::real_time *out_mtime, std::map *out_attrs, bufferlist *out_data, bufferlist *out_omap_header, bufferlist *out_omap_data, vector *out_snaps, snapid_t *out_snap_seq, uint32_t *out_flags, uint32_t *out_data_digest, uint32_t *out_omap_digest, vector > *out_reqids, uint64_t *truncate_seq, uint64_t *truncate_size, int *prval) {} void undirty() {} struct C_ObjectOperation_isdirty : public Context {}; void is_dirty(bool *pisdirty, int *prval) {} void omap_get_header(bufferlist *bl, int *prval) {} void omap_set(const map &map) {} void omap_set_header(bufferlist &bl) {} void omap_clear() {} void omap_rm_keys(const std::set &to_remove) {} // object classes void call(const char *cname, const char *method, bufferlist &indata) {} void call(const char *cname, const char *method, bufferlist &indata, bufferlist *outdata, Context *ctx, int *prval) {} void rollback(uint64_t snapid) {} void copy_from(object_t src, snapid_t snapid, object_locator_t src_oloc, version_t src_version, unsigned flags, unsigned src_fadvise_flags) {} }; OSDOp osd_types.h struct OSDOp { ceph_osd_op op; // 操作 sobject_t soid; // oid bufferlist indata, outdata; // 输入输出data int32_t rval; // 返回码 }; Objecter class Objecter : public md_config_obs_t, public Dispatcher { public: Messenger *messenger; // 消息 MonClient *monc; // mc Finisher *finisher; private: OSDMap *osdmap; // osdmap public: using Dispatcher::cct; std::multimap crush_location; atomic_t initialized; private: atomic64_t last_tid; atomic_t inflight_ops; atomic_t client_inc; uint64_t max_linger_id; atomic_t num_unacked; atomic_t num_uncommitted; atomic_t global_op_flags; // flags which are applied to each IO op bool keep_balanced_budget; bool honor_osdmap_full; public: void maybe_request_map(); private: void _maybe_request_map(); version_t last_seen_osdmap_version; version_t last_seen_pgmap_version; mutable boost::shared_mutex rwlock; using lock_guard = std::unique_lock; using unique_lock = std::unique_lock; using shared_lock = boost::shared_lock; using shunique_lock = ceph::shunique_lock; ceph::timer timer; PerfCounters *logger; uint64_t tick_event; void start_tick(); void tick(); void update_crush_location(); public: /*** track pending operations ***/ // read public: struct OSDSession; struct op_target_t {} // 操作目标 struct Op : public RefCountedObject {}; // 操作 }; op_target_t 操作目标,封装pg信息,osd信息 struct op_target_t { int flags; object_t base_oid; object_locator_t base_oloc; object_t target_oid; // 目标oid object_locator_t target_oloc; // 位置 // 是否 base_pgid bool precalc_pgid; ///< true if we are directed at base_pgid, not base_oid // 直接的 pgid pg_t base_pgid; ///< explciti pg target, if any pg_t pgid; ///< last pg we mapped to unsigned pg_num; ///< last pg_num we mapped to unsigned pg_num_mask; ///< last pg_num_mask we mapped to // 启动的osd vector up; ///< set of up osds for last pg we mapped to // acting osd vector acting; ///< set of acting osds for last pg we mapped to // primary int up_primary; ///< primary for last pg we mapped to based on the up set int acting_primary; ///< primary for last pg we mapped to based on the /// acting set // pool 大小 int size; ///< the size of the pool when were were last mapped // pool 最小size int min_size; ///< the min size of the pool when were were last mapped // 是否按位排序 bool sort_bitwise; ///< whether the hobject_t sort order is bitwise // 是否副本 bool used_replica; bool paused; int osd; ///< the final target osd, or -1 }; 操作 struct Op : public RefCountedObject { OSDSession *session; // session 连接 int incarnation; op_target_t target; // 操作目标 ConnectionRef con; // for rx buffer only uint64_t features; // explicitly specified op features vector ops; // 操作集合 snapid_t snapid; SnapContext snapc; ceph::real_time mtime; bufferlist *outbl; vector out_bl; vector out_handler; vector out_rval; int priority; Context *onack, *oncommit; uint64_t ontimeout; Context *oncommit_sync; // used internally by watch/notify ceph_tid_t tid; eversion_t replay_version; // for op replay int attempts; version_t *objver; epoch_t *reply_epoch; ceph::mono_time stamp; epoch_t map_dne_bound; bool budgeted; /// true if we should resend this message on failure bool should_resend; /// true if the throttle budget is get/put on a series of OPs, /// instead of per OP basis, when this flag is set, the budget is /// acquired before sending the very first OP of the series and /// released upon receiving the last OP reply. bool ctx_budgeted; int *data_offset; epoch_t last_force_resend; osd_reqid_t reqid; // explicitly setting reqid }; 分片 Striper 扩展 ObjectExtent 记录分片信息 class ObjectExtent { public: object_t oid; // object id uint64_t objectno; // 序号 uint64_t offset; // in object object内偏移 uint64_t length; // in object 分片长度 uint64_t truncate_size; // in object object_locator_t oloc; // object locator (pool etc) pool位置 vector > buffer_extents; // off -> len. extents in buffer being mapped (may be fragmented bc of striping!) };
来源:OSCHINA
发布时间:2019-03-23 18:45:00
Redis是一个高性能的、开源免费的、遵守BSD协议的key-value数据库。有以下三个特点:第一,Redis支持数据的持久化,可以将内存中数据保存在磁盘中,重启的时候可以再次加载进行使用;第二,Redis不仅仅支持简单的key-value类型的数据,同时还支持string、list、set、sorted set(zset)、hash数据类型;第三,Redis支持数据的备份,即master-slave模式的数据备份。
来源:OSCHINA
发布时间:2020-03-26 10:38:00
本文作者:AIOps智能运维 干货概览 人生病了要去看医生,程序生病了看的就是运维工程师了。医生给病人看病要做很多检查,然后拿着结果单子分析病因。运维工程师分析系统故障也会查看采集的监控指标,然后判断根因是什么。 查看指标这事儿,说起来也不难。只要画出指标的趋势图(指标值随时间变化的曲线),有经验的工程师很容易就能看出有没有毛病,进而推断故障的原因。不过呢,都说脱离开剂量说食物的毒性是耍流氓,查看指标这事也差不多。如果只有几条指标需要查看,做个仪表盘就能一目了然,可是如果有成千上万的指标呢?人家查抄大老虎的时候点钞机都烧坏了好几台,如果人工查看这么多指标,脑子的下场估计也好不到哪儿去。所以说还是得靠“机器人”。 等等,“机器人”怎么能知道什么指标有毛病,什么指标没毛病呢?就算能知道,把有毛病的指标挑出来工程师凭啥就能知道根因呢?所以,我们的“机器人医生”必须能够识别出指标的异常,然后还需要能把识别出的异常整理成工程师容易理解的报告才行。 传统的办法 人工诊断故障的时候,工程师往往是根据脑子里的模块调用关系图(图 1)来排查系统。很多时候,故障都是因为在最上游的前端模块(图 1中的A)上看到了很多失败的请求发现的。这时,工程师就会沿着A往下查。因为A调用了B模块,所以需要查看B的指标,如果有指标异常那么就怀疑是B导致了故障。然后再检查B的直接下游模块C,以此类推。在这个过程中,怀疑通过模块的调用关系不断往下传递,直到传不下去为止。在图 1的例子中怀疑最后就停在了倒霉蛋G的头上,谁让它没有下游模块呢。 总的来说,这就是模块间把责任想办法往下游推的过程。当然,真实的场景要更加复杂一些。并不是只要下游有异常就可以推的,还需要考察异常的程度。比如,如果倒霉蛋G的异常程度比E的异常程度小很多,根因就更有可能在E里面。 找到了根因模块再去分析根因就容易多了,所以寻找根因模块是故障诊断中很重要的步骤。 上面的过程可以很直接地变成一个工具: 做一个页面展示模块调用关系图 工程师为每个指标配置黄金指标,以及黄金指标的阈值 在模块图中标出黄金指标有异常的模块以及它们到达前端模块的可能路径 这个工具通过配置黄金指标及阈值的方式解决了指标以及如何判断异常的问题,然后再通过模块调用关系图的方式呈现异常判断的结果,解决了异常判断和结果整理这两个核心问题。 不过,传统的办法在实际使用中还是会碰到很多问题: 1.活的系统一定是不断演化的,模块的调用关系也随之发生改变。为了保证工具里面的关系图不会过时,就需要不断从真实系统同步。干过系统梳理这种活的工程师都知道,这可不容易。如果整个系统使用统一的RPC中间件在模块中通讯,那就可以通过分析RPC trace log的方式挖掘出调用关系图来,不过“历史代码”通常会趴在路中间拦着你。 2.每个黄金指标通常只能覆盖一部分的故障类型,新的故障一出现,就需要增加黄金指标。这样一来配置工作——尤其是阈值的配置——就会不断出现。另外,指标多了,就很容易出现“全国山河一片红”的情况。大多数的模块都被标出来的时候,工具也就没啥用了。 3.大型的系统为了保证性能和可用性,常常需要在好几个机房中部署镜像系统。因为大多数的故障只发生在一个机房的系统中,所以工程师不但需要知道根因模块是谁,还需要知道在哪个机房。这样一来,每个机房都得有一个调用关系图,工程师得一个一个地看。 理想的效果 传统的方法作出来的诊断工具最多也就是半自动的,应用起来也受到很多的限制,所以我们就想做一个真正全自动、智能化的工具。 首先,我们希望新工具不要过于依赖于黄金指标,这样指标的配置工作就能减少。但是,这反过来说明全自动的工具必须能够扫描所有模块上的所有指标,这样才能做到没有遗漏。所以,异常判断不能再通过人工设置阈值的方式来进行,而必须是基本上无监督的(Unsupervised)。另外,不同指标的语意有很大差异,异常判断的算法也必须足够灵活,以适应不同指标的特点。 其次,我们希望工具不要太过依赖于调用关系图,这意味着我们需要寻找一种新的方式来整理和呈现结果。其实,调用关系图并不是必须的。在使用传统诊断方法时,我们就发现一部分工程师经常脱离调用关系图,直接按照黄金指标的异常程度从大到小检查模块。这是因为这部分工程师负责的系统黄金指标代表性强、容易理解,更重要的是不同模块黄金指标的异常程度可以比较。 所以说,我们完全可以做一个诊断工具来产出根因模块的推荐报告,报告的内容必须易于理解,推荐的顺序也必须足够准确。 实例指标的自动排查分析 我们以实例指标为例,介绍如何实现一个指标排查工具,达成理想的效果。 在第一步,所有被收集来的指标都会通过异常检测算法赋予它们一个异常分数。比较两个指标的异常分数就能够知道它们的异常程度谁大谁小了。这一步的核心是要寻找一个方法能够量化地衡量每个指标的异常,而且这个量化衡量出来的分数还可以在不同实例的不同指标之间比较。 第二步,我们把异常分数按照它们所属的实例分组,每组形成一个向量(vector)。这时,每个实例都会对应一个向量,向量中的每个元素就是一个指标的异常分数。然后,模式(pattern)差不多的向量就可以通过聚类(clustering)算法聚成若干个摘要(digest)。这样一来,工程师们就容易理解分析的结果了。 第三步,我们可以根据摘要中包含的实例以及指标的异常分数排序(ranking),形成推荐报告。 总结 本文介绍了一种在服务发生故障时自动排查监控指标的工具,第一步利用了概率统计的方式估算每个指标的异常分数,第二步用聚类的方式把异常模式相近的实例聚集在一起形成摘要,第三步用ranking的方式向工程师推荐最有可能是根因的摘要。 由于运维场景的特点是数据量大,但是标定很少,生成标定的代价高昂而且容易出错,接下来我们会详细介绍如何利用概率统计、非监督学习和监督学习的方法来解决这个问题,敬请期待吧~ 原文链接地址: https://developer.baidu.com/topic/show/290066
来源:OSCHINA
发布时间:2019-03-30 12:21:00
session cluster和per job 因为是源码分析,所以会分为服务端和客户端两个部分的代码分析,下面我先看服务端
session cluster模式是类似standalone,先去向yarn申请好资源,然后供业务方提交,主要的入口类是YarnSessionClusterEntrypoint(这里指的是服务端的入口)

从上图可以看出来,startCluster()方法前后是两个分界线,startCluster之前是获取配置,之后是进行集群相关的创建,包括haService/blobServer/heartBeatService/resourceManger/webMonitorEndpoint。
这里有一点是需要说明的是有关executionGraphStore, 这里实际有两种, 1.将可执行图放在内存中, 2.将可执行图持久化到文件。
yarn session:将executionGraph持久化到文件
per job:将executionGraph持久化到文件
对于per job模式是每个任务对应一个集群,其实就是将上图中的YarnSessionClusterEntrypoint改成YarnJobClusterEntrypoint,其它流程基本一致(除去executionGrap的存储)。
下面来看一下两个主类的继承关系图

从图上可以看到主要的区别就是createSerializeableExecutionGraphStore方法,也就是executionGraph的存储位置不同。
session client和per job 由于flink不同的版本代码变动较大,所以在这里需要区分flink的版本进行一下说明 flink1.9之前的基本一致,提交至yarn的主要流程都在CliFrontend和FlinkYarnSessionCli中, 我们来看一下主要流程

这里session和per job的在流程上的最大区别就是clusterId是否为空 flink1.9之后进行了流程统一,抽象出了一个PipelineExecutor接口,统筹所有的提交,不过在看继承关系之前还是先看一下yarn-client的提交流程其实主要入口还是CLiFrontened,不过在加载完配置文件之后就直接反射调用invokeInteractiveModeForExecution,这个类会调用用户的main函数,加载完用户业务代码之后,会去走正常的提交流程。 到这里已经将所有的提交流程都说完了,大家对于flink争个提交流程应该有了更加清晰的认识。
最后在来说一下flink submit的接口,这是在flink-1.10才出现的一个新的统一,流程图如下

从上图可以看出来,AbstractSessionClusterExecutor中的主要调用逻辑其实和上面我们已经看到的session cluster的提交流程是一致的,只不过代码更加的抽象,这样其实扩展性也更加好,AbstractJobClusterExecutor主要主要就是为了向已有集群提交任务的,LocalExecutor其实是为了用户本地调试所用 欢迎关注我的公众号
来源:OSCHINA
发布时间:2020-03-26 10:31:00
本文作者:AIOps智能运维 干货概览 AIOps(Artificial Intelligence for IT Operations ),即智能运维,是将人工智能的能力与运维相结合,通过机器学习的方法来提升运维效率。 在传统的自动化运维体系中,重复性运维工作的人力成本和效率问题得到了有效解决。但在复杂场景下的故障处理、变更管理、容量管理、服务资源过程中,仍需要人来掌控决策的过程,这阻碍了运维效率的进一步提升。而AI方法的引入,使得机器能够代替人来做出决策,从而让真正意义上的实现完全自动化成为了可能。 在AIOps的落地实施过程中,最关键的因素还是人,即AIOps的建设者们。 AIOps作为一个全新的技术发展和应用方向,并不是简单地说具备某一种技能或招募一两个大牛就可以完成的,它需要不同角色、多个团队的配合才可以达成。根据近几年来整个业界对AIOps的理解和实践,AIOps参与角色的划分也越来越清晰。在百度4年的AIOps实践中,我们总结得出了如下四种不可或缺的角色: 运维工程师 运维研发工程师 平台研发工程师 运维AI工程师 可以看到,除了运维AI工程师外,其他角色并不是AIOps产生之后才出现的,他们在传统运维中也发挥了重要作用。我们今天主要想和大家探讨一下,在AIOps时代,他们的职责究竟发生了哪些变化。为了方便大家理解,我们会基于百度AIOps的实践案例,来进行具体说明。 单机房故障自愈场景 单机房故障自愈是一个典型的AIOps落地项目。该方案主要解决的问题场景如下:某个业务由于网络、设备、变更、程序Bug、容量等原因造成故障,但故障范围仅局限在单个机房或单个Region内部。那么,我们可以基于流量调度等手段,将访问流量调度到非故障机房或Region,实现该类型故障的自动止损。 在这个过程中,需要AIOps四种角色分工明确、紧密配合,来完成整个AIOps解决方案的落地实现。 运维工程师 在单机房故障自愈项目中,运维工程师基于日常运维工作中所积累的场景、问题和经验,确定以单机房故障止损作为主要需求和突破口,通过定义单机房故障止损的问题域、解决思路以及风险点,明确AI可以发力的领域。 在完成问题域的定义后,运维工程师需要跟踪整个单机房故障自愈解决方案的落地,包括在策略设计前期提供数据标注支持,在中期进行效果的验收,在后期将单机房故障自愈方案实际部署运行到生产环境。 AIOps时代的职责和技能变化 运维工程师承担线上服务质量的责任,是服务质量的关键保证。在工作过程中,会与研发、产品、运营等各类角色、不同团队进行深度的沟通和协作。 传统运维中,运维工程师的主要职责分为三个方面:质量、成本、效率。 在AIOps落地实施中,运维工程师是处于中心的角色,也赋予了新的职责,他们是AIOps具体实施的需求提出者和成果验收者。具体职责包括: 在AIOps时代,运维工程师一方面需要熟悉运维领域的知识,了解运维的难题和解决思路;另一方面需要了解人工智能和机器学习的思路,能够理解哪些场景问题适合用机器学习方法解决,需要提供怎样的样本和数据,即成为AI在运维领域落地实施的解决方案专家。 运维AI工程师 在单机房故障自愈场景中,运维AI工程师将机器学习的算法与实际的故障处理业务场景相结合,针对单机房故障场景的风险点,进行策略研发与实验工作。如下图所示: 运维AI工程师分别设计了如下算法策略来满足整个复杂故障场景的自动决策: 异常检测算法:解决故障发现时指标异常判断问题,基于AI方法实现较高的准确率和召回率,作为整个故障自愈的数据基础。 策略编排算法:基于当前线上的实际流量和服务状态,设计损益计算模型,判断基于何种方式的操作组合或步骤,能够使整个自动止损带来收益最大,风险最小。 流量调度算法:基于线上服务容量与实时流量情况,进行精确流量比例计算,防御容量不足或不准风险,并实现流量调度收益最大化。 在完成策略设计与研发后,需要根据历史数据进行Case回溯,并进行仿真Case模拟,来验证策略效果,并进行逐步迭代调优,以达到线上运行的准确率和召回率要求。 AIOps时代的职责和技能变化 运维AI工程师是将AI引入运维的核心角色。他们针对运维数据、运维经验进行理解和梳理,使用机器学习的方法将海量运维数据进行汇总、归纳,使得数据中的价值显现出来。 运维AI工程师首先需要具备AI工程师的技能,需要对数学及机器学习方法有足够的掌握程度,并能应用实践。 如单机房故障自愈场景中的介绍,运维AI工程师需要具备机器学习知识并在运维领域落地的能力。 平台研发工程师 在单机房故障自愈场景中,平台研发工程师需要关注三类平台的建设。 基础运维平台:提供单机房故障自愈场景中的依赖平台,如:监控平台和流量调度平台。在日常运维中提供标准化运维数据获取和运维操作的基础,而在AIOps中,这部分接口需要能够同时支持人工和自动的数据获取和运维操作。 智能运维平台:提供对AI能力的支持,如:统一的数据服务(运维知识库)、运维开发框架,以及给AI策略实验和运行的运维策略框架等。 故障自愈机器人:针对单个业务场景进行平台化抽象,使之成为一个基础服务,基于AIOps平台研发和运行。 AIOps时代的职责和技能变化 平台研发工程师负责运维平台及基础组件的研发与建设。 在传统运维场景中,平台研发工程师负责平台、基础组件、类库和工具的研发工作。在针对运维的场景中,会覆盖运维相关的服务管理、监控、变更、流量调度等相关平台。 这部分平台是运维的基础,在AIOps时代仍然需要依赖于这些平台的建设。 同时在AIOps场景中,数据成为了中心,运维各种状态信息转换为大数据,机器学习则作用在大数据上进行分析。在百度AIOps的实践中,运维开发框架、运维知识库、运维策略框架共同组成了完整的智能运维平台,三大平台的建设和实施离不开大数据、机器学习架构的引入。这就要求平台研发工程师具备大数据、机器学习平台架构师的多重身份,具备流式计算、分布式存储、机器学习平台、算法策略平台等一系列大数据和机器学习平台架构能力。 运维研发工程师 基于多个业务线场景抽象出的单机房故障自愈解决方案,能够满足大部分场景需求,但并不意味着可以直接提供给各个业务线来使用。原因如下: 策略和参数需要进行调整 流量调度、容灾策略等策略,针对不同的业务线,配置并不相同。例如某些业务对响应时间敏感,跨地域的调度会带来较大的延迟,影响用户体验,这时就需要根据业务情况配置机房之间的跨机房流量调度延迟系数,来实现流量优先调度到延迟系数最低的机房。 通用框架无法满足所有需求 部分业务线需要对原有的策略进行部分重写才能够满足需求。例如,部分业务在流量调度时,需要联动服务降级来满足容量需求,这就需要额外增加服务降级联动的逻辑。 那么,就需要运维研发工程师出手来解决这个问题。根据业务线的实际情况,对策略和参数进行配置和调优,对通用框架无法满足的需求,进行定制化研发,使得单机房故障自愈方案能够实际应用在不同业务线上。 AIOps时代的职责和技能变化 运维研发工程师负责基于业务线特征的运维研发工作,在传统运维中,是运维自动化的实施者,实现了针对业务场景的自动化运维实施落地。 在AIOps时代,运维研发工程师承担了AIOps智能化运维解决方案在业务线实施落地的职责。他们是AIOps场景的实践者,将AIOps解决方案与业务架构特征相结合,实现AIOps在业务线的落地。 一方面,他们会与运维工程师紧密配合,对业务问题进行深度分析,理解业务的特点。另一方面,他们与平台研发工程师、AI工程师相配合,基于AIOps解决方案的策略和框架,进行定制化开发,使其适合自身业务线的特征。 总结 本文介绍了运维工程师、运维AI工程师、平台研发工程师、运维研发工程师四种角色在自动化运维时代和AIOps智能化运维时代,其职责和技能的拓展和变化。AIOps技术为运维技术的发展带来了更多的机遇,对于每个参与到AIOps实施的个人或团队也是如此。四种角色既有术业专攻,同时又紧密协作,共同将AI能力引入为运维赋能。那么,你的选择是什么呢? 原文链接地址: https://developer.baidu.com/topic/show/290065
来源:OSCHINA
发布时间:2019-03-30 12:10:00
本文作者:AIOps智能运维 干货概览 在运小皮《百度自动化运维演讲》文章中提到,2014年以来,百度运维开始向智能化方向迈进。智能运维时代,如何提高智能运维效率,降低通用运维操作(典型如故障场景)开发难度和成本,成为首要难题。本文将向大家介绍面向感知、决策、执行的百度智能运维工程化解决方案。 背景介绍 故障处理和操作变更是运维两大主题。在过去,为维护系统稳定,各业务线都投入大量人力进行故障处理工作,除直接人肉运维外,各产品线深度定制的运维工具、系统被研发出来。随着业务规模扩张和形态变迁,传统运维模式受到极大挑战: 无统一的开发管理模式,运维服务开发及维护成本大,运维效率低。 横向扩展能力差,运维经验难以复用,各产品线”重复造轮子”。 智能运维开发框架,提供了一种以软件工程方式解决运维问题的解决方案。通过提供统一的开发模型和管理机制,支持不同产品线运维操作的设计、实现和管理。从而: 降低设计、开发难度与成本,使业务OP专注自身的业务逻辑,提高开发和迭代效率。 促进基于代码的跨产品线经验积累与分享,提升百度整体的业务运维能力。 充分运用和发挥自动控制、机器学习、人工智能等领域的技术成果,提高运维效率。 解决思路 智能运维开发框架以Noah(百度自动化运维管理平台)时代的运维经验为基础,通过对运维概念和操作的统一,整合当前运维系统,提供运维操作的统一入口;让更多的业务线OP加入到运维社区建设中,共享运维经验,满足业务日益多样化的需要。 具体解决思路如下: 1运维模式标准化 统一开发模式:提供统一的开发规范,社区化开发模式,业务线OP共同参与运维操作开发,沉淀运维经验。 统一运维对象:通过知识库,统一描述机器、实例、服务、应用等运维对象的属性,聚集分散的运维状态数据,达到公司内运维对象的统一。 统一运维操作:屏蔽具体平台操作实现,提供统一的运维对象操作接口。 2运维开发工程化 提供统一的运维开发框架:封装常用功能组件,提供高扩展的开发框架,使产品线专注于自身业务逻辑,开发”智能运维机器人”。 提供仿真系统:通过提供服务拓扑搭建及模拟故障的能力,完成机器人上线前功能验证,提高”机器人”可靠性。 提供托管平台:通过提供高可用的机器人托管环境,降低服务运维成本。 3运维操作智能化 智能感知:依赖监控系统提供的智能异常检测、多维度异常分析,感知满足时效性和准确度的异常事件。 智能决策:自定义算法实现决策机制,充分利用机器学习、人工智能成果,提供决策可靠性。并沉淀人对问题的决策经验,做到经验可迁移。 智能执行:提供丰富的执行策略,满足业务线通用运维操作的需求。 实现方案 整体解决方案如下: 以智能运维机器人为主体,深度整合公司内代码管理工具,持续交付平台,部署系统等devops工具链,帮助产品线同学快速完成源码构建、镜像打包、应用部署,提供开发、测试、运维整套解决方案,大幅提升开发效率。 智能运维开发框架自身提供的功能如下: 智能运维开发框架提供了高扩展、易使用的智能运维机器人开发框架,具备线上服务拓扑结构搭建和query级别异常模拟能力的仿真系统,具备单地域故障处理能力的高可用服务部署托管平台,完成开发至上线流程的全覆盖,用户只需要在智能运维开发框架基础上嵌入自己的业务代码,即可完成满足自身业务的运维操作。 总结 智能运维开发框架以变革运维模式为目标,提供了开发、验证、运维工程化解决方案。一经上线,便作为各类故障自愈、高可用架构项目的基础支撑,大幅提高了项目开发效率,减小了开发难度和成本,表现出了极强的稳定性。 相信在不久的将来,智能运维开发框架会成为百度运维操作的载体,不断达成智能运维的使命。 智能运维开发框架的具体实现和最佳实践将在后续文章中详细介绍,敬请期待! 原文链接地址: https://developer.baidu.com/topic/show/290064
来源:OSCHINA
发布时间:2019-03-30 11:59:00
hdfs的集群中的三种角色:Namenode(NN),Datanode(DN),SecondNamenode(SN) Namenode的工作机制 NN是hdfs的管理节点,主要职责包括:1.管理文件系统的命名空间,维护着hdfs中的虚拟文件目录树结构。2.保存文件系统中所有文件和目录元数据信息(机制复杂,内存中一份完整的,fsimage和edits log加起来是一份完整的)。3.相应客户端的请求,无论读写hdfs都会先访问NN,节点的交互细节都被封装在FileSystem类中了。4.NN中也会记载每个文件的各个块所在数据节点信息,但是NN不会持久化保存文件块与节点的映射信息,因为这些信息会在系统启动时候根据数据节点汇报上来的信息进行重建。 客户端向集群中存储数据之前,首先需要访问NN申请写入文件,NN检测对应的虚拟文件是否存在,如果不存在就向客户端返回可以存入并返回分配的DN,客户端对文件进行切分,将各个block存入NN返回的DN列表中写入。而一个blk的多个副本是由DN向其他的节点写入的,而不是由客户端来直接写入的,当DN在写某个副本时候失败了,会将失败信息返回给NN,NN会重新分配DN进行副本写入。文件的最后一个blk可能不满128M。但是这样的一个blk也要在NN中含有一条元信息记录(一个blk的元数据信息大约150B)。所以hdfs不善于存储小文件,因为小文件耗费NN的存储空间,MapReduce的性能也降低。 向集群中读写文件,都需要访问NN,所以NN的负载很大。那么如何提高NN的相应速度呢?每次对NN的访问,都会涉及到元数据的读取,为了尽可能快速读取元数据,可以将元数据全放内存,但是内存是遗失性存储,这样做安全性无法得到保证,但是元数据全放磁盘则查询又会太慢。 所以hdfs维护了两份元数据:内存中一份元数据(为了提高查询速度,所以也可以看到,NN需要将元数据全部都放在内存中,所以内存大小可能会限制整个hdfs的文件存储规模),磁盘中一份数据fsimage(为了元数据持久化),还有一个用于记录hdfs集群最新操作日志的小文件edits log。内存中的元数据和fsimage不总是完全一致的,内存中的元数据总是领先fsimage一个edits log的内容。因为最新的元数据总是先写入edits log,写入成功后让客户端去写文件,文件写入成功之后,NN将这份最新的元数据加载到内存中。当SN执行checkpoint操作时,会将fsimage和editslog的内容合并成新的新的fsimage,这个时候fsimage和内存中的元数据几乎相同(可能仍然还差着一个edits.new)。 edits log是一个小文件(默认是64M),并不能存太多的数据,以免影响写入速度。所以当edits log写满了的时候,则应该将edits log中的数据全部合并到fsimage中。但是edits log是日志格式,与fsimage的格式不一样,所以需要一定的资源用于合并,如果这个任务交给NN,则会加大NN的压力,所以引入了SN来解决fsimage和edits log合并的问题。当edits log写满时或者到了一个额定时间(默认3600s),NN通知SN来做checkpoint操作合并fsimage和edits log。SN进行check point操作:首先通知NN不要继续向原来edits中写入新数据了,NN把最新的元数据写入到edits.new中,然后SN从NN下载fsimage和edits,SN将fsimage导入内存,然后对其应用edits中的日志操作,最后生成一个fsimage.checkpoint文件并保存,然后将fsimage.checkpoint上传到NN。NN用将fsimage.checkpoint重命名为fsimage,edits.new重命名为edits,以替换掉旧的fsimage和edits。 以上的这一套机制,只能做到数据可靠而无法保证系统高可用。猜想:双NN,其中一个作为完全热备,但是需要保证双NN上的元数据的一致性。 Datanode的工作机制 DN提供真实数据的存储服务和数据库检索服务,DN会定期向NN发送他们所存储的块的列表。hdfs参数dfs.block.size能够控制块大小,默认是128M。即使一个文件没有达到block的大小,仍然会占用一条元数据。所以说hdfs最好存大文件,小文件比较费NN的元数据存储空间。hdfs存储文件block不会添加任何额外的内容,完全就是按照字节数来切分,到一个大小就切,到一个大小就开始切。 HDFS的java客户端编写 启动centos的图形界面命令:init 5或者startx eclipse添加依赖包的过程:java build path->Libraries->Add Library->User Library->User libraries->New->add extermal JARs import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.net.URI; import org.apache.commons.io.IOUtils; import org.apache.hadoop.conf.Configuration; import org.apache.hadoop.fs.FSDataInputStream; import org.apache.hadoop.fs.FSDataOutputStream; import org.apache.hadoop.fs.FileStatus; import org.apache.hadoop.fs.FileSystem; import org.apache.hadoop.fs.LocatedFileStatus; import org.apache.hadoop.fs.Path; import org.apache.hadoop.fs.RemoteIterator; import org.junit.Before; import org.junit.Test; public class HdfsUtil { FileSystem fs = null; public void init() throws Exception{ //读取classpath下的xxx-site.xml 配置文件,并解析其内容,封装到conf对象中 Configuration conf = new Configuration(); //也可以在代码中对conf中的配置信息进行手动设置,会覆盖掉配置文件中的读取的值 conf.set("fs.defaultFS", "hdfs://weekend110:9000/"); //根据配置信息,去获取一个具体文件系统的客户端操作实例对象,记得hdfs的权限问题 fs = FileSystem.get(new URI("hdfs://weekend110:9000/"),conf,"hadoop"); } /** * 上传文件,比较底层的写法 * * @throws Exception */ public void upload() throws Exception { init(); Path dst = new Path("hdfs://weekend110:9000/aa/qingshu.txt"); FSDataOutputStream os = fs.create(dst); FileInputStream is = new FileInputStream("c:/qingshu.txt"); IOUtils.copy(is, os); } /** * 上传文件,封装好的写法 * @throws Exception * @throws IOException */ public void upload2() throws Exception, IOException{ init(); fs.copyFromLocalFile(new Path("c:/qingshu.txt"), new Path("hdfs://weekend110:9000/aaa/bbb/ccc/qingshu2.txt")); } /** * 下载文件,比较底层的写法 * @throws Exception */ public void download2() throws Exception { init(); FSDataInputStream is = fs.open(new Path("/jdk-7u65-linux-i586.tar.gz")); FileOutputStream os = new FileOutputStream("c:/jdk7.tgz"); IOUtils.copy(is, os); } /** * 下载文件,封装好的写法 * @throws Exception * @throws IllegalArgumentException */ public void download() throws Exception { init(); fs.copyToLocalFile(new Path("hdfs://weekend110:9000/aa/qingshu2.txt"), new Path("c:/qingshu2.txt")); } /** * 查看文件信息 * @throws IOException * @throws IllegalArgumentException * @throws FileNotFoundException * */ public void listFiles() throws FileNotFoundException, IllegalArgumentException, IOException { init(); // listFiles列出的只是文件信息,不会列出文件夹的信息,可以对文件夹递归遍历 RemoteIterator files = fs.listFiles(new Path("/"), true); while(files.hasNext()){ LocatedFileStatus file = files.next(); Path filePath = file.getPath(); String fileName = filePath.getName(); System.out.println(fileName); } System.out.println("---------------------------------"); //listStatus 可以列出文件和文件夹的信息,但是不提供自带的递归遍历 FileStatus[] listStatus = fs.listStatus(new Path("/")); for(FileStatus status: listStatus){ String name = status.getPath().getName(); System.out.println(name + (status.isDirectory()?" is dir":" is file")); } } /** * 创建文件夹 * @throws Exception * @throws IllegalArgumentException */ public void mkdir() throws IllegalArgumentException, Exception { init(); fs.mkdirs(new Path("/aaa/bbb/ccc")); } /** * 删除文件或文件夹 * @throws IOException * @throws IllegalArgumentException */ public void rm() throws IllegalArgumentException, IOException { init(); fs.delete(new Path("/aa"), true); } public static void main(String[] args) throws Exception { download(); } } fileSystem设计思想 将所有的文件系统抽象成了一个FileSystem之后,访问具体的对象时候,只管调用方法,不关心底层的实例对象到底是谁,这样MapReduce程序与底层的文件系统解耦合了。 hadoop框架中的RPC调用机制 RPC 远程过程调用,主要应用于分布式系统。比如有三台机器,a,b,c。a是一个客户端,访问了b,b中没有请求的服务,这个服务在远程的c机器上。b通过RPC机制能够向服务在本地一样地去调用c上的服务程序。具体如下:b将调用的服务类,方法,方法的参数等信息封装以下通过本地的socket client发送给c上的socket server,然后c机器根据调用信息,使用反射机制,获得一个服务类对象,调用相应的方法,然后再将方法的执行结果通过本地socket client发送给b上的一个socket server,然后b将这个结果解析出来。 NN和client,NN和DN,DN和DN之间很多通信,使用了大量的RPC通信机制,例如DN会定期的向NN报告本身的block情况。HADOOP实现了一个RPC框架——代理类机制(使用了动态代理和反射和socket技术)。1.生成调用端socket程序的动态代理对象。2.调用动态代理对象的业务方法。3.调用socket的请求方法。4发送调用请求。5.服务端开始了:生成业务类的动态代理对象。6.调用业务类动态代理对象的具体业务方法。7.获取调用结果。8.返回调用结果给服务端。9.给原始调用者返回结果。 Hadoop的RPC框架调用 主要的以依赖包都在share/common中。使用hadoop的RPC框架的最基本的部分:1.协议(表现为java接口),代理类和业务类都需要实现这个接口已确保方法的一致性。值得注意的是在协议接口中需要指定一个 public static final long versionID 的协议版本号,后面代理类调用方法时候,会用到这个参数,用来验证代理类和业务类的协议版本的一致性。2.业务类:一般类名以Impl结尾,在业务类中需要实现协议接口中的所有方法,这些被实现的方法都会以服务的形式发布出去。3.controller类,controller类接收客户端的请求,利用RPC.getProxy()方法生成一个代理类对象,然后用这个代理类对象调用一个协议接口中的方法,通过RPC机制远程调用业务类的方法,得到结果再返回给客户端。4.Starter类这个类名随意,主要的作用是将业务类中实现的协议接口的方法发布为一个服务:新建一个RPC.Builder类对象,然后设置服务的地址,端口,协议以及业务类,之后利用builder对象获得一个server对象,再启动起来就ok了。 1.协议接口 接口实现: public interface LoginServiceInterface { public static final long versionID=1L;//用于协定协议的版本号,RPC调用的时候会用到 public String login(String username,String password); } 2.服务端 业务类的实现: public class LoginServiceImpl implements LoginServiceInterface { @Override public String login(String username, String password) { return username + " logged in successfully!"; } } 启动类的实现: import java.io.IOException; import org.apache.hadoop.HadoopIllegalArgumentException; import org.apache.hadoop.conf.Configuration; import org.apache.hadoop.ipc.RPC; import org.apache.hadoop.ipc.RPC.Builder; import org.apache.hadoop.ipc.RPC.Server; public class Starter { public static void main(String[] args) throws HadoopIllegalArgumentException, IOException { Builder builder = new RPC.Builder(new Configuration()); builder.setBindAddress("weekend110").setPort(10000).setProtocol(LoginServiceInterface.class).setInstance(new LoginServiceImpl()); Server server = builder.build(); server.start(); } } 客户端 import java.net.InetSocketAddress; import org.apache.hadoop.conf.Configuration; import org.apache.hadoop.ipc.RPC; public class LoginController { public static void main(String[] args) throws Exception { LoginServiceInterface proxy = RPC.getProxy(LoginServiceInterface.class, 1L, new InetSocketAddress("weekend110", 10000), new Configuration()); String result = proxy.login("mijie", "123456"); System.out.println(result); } } 思考题:服务的动态转发和负载均衡???zookeeper hdfs 源码分析 FileSystem对象的创建过程(创建与NN进行通信的RPC代理类) hdfs的一个核心类:FileSystem,fs必要的成员:1.rpcProxy代理类,这个代理类应该实现一个接口clientProtocal 源码重点:如何根据conf来获取对应的fs,获取fs时候,如何获得RPCProxy代理类对象。 首先获取conf中的URI,然后将URI解析成scheme(hdfs)和authority(weekend110:9000) getInternal方法:单例模式,懒汉模式。先从一个Map中根据Key来获取,如果没有获取到,再去创建Filesysytem createFileSystem方法:获取对应的fileSystem的class,在反射出来一个相应对象(这个对象是空的,里面的各个数据域都是基本值) fs.initalize()方法:这是具体实现类的方法,设置DFS类的dfs,uri,workDir。设置dfs时候是构造了一个DFSClient对象。dfs的namenode就是rpc代理对象。 总结:根据conf对象,利用反射机制拿到DistributedFileSystem(是FileSystem的一个继承子类)对象,然后设置fs的各个数据域,其中有一个很重要的数据域 打开输入流的过程(创建与DN进行通信的RPC代理类) 先利用那个namenode代理类与NN通信,拿到文件的元信息(各个block的位置) DFSInputStream的blockReader是与DN进行交互获得各个blk的详细过程。 locatedBlocks记录各个blk的详细信息。
来源:OSCHINA
发布时间:2020-03-25 23:02:00
本文作者:AIOps智能运维 背景:为什么要做智能运维 百度云智能运维团队在运维工具和平台研发方向历史悠久,支撑了全百度数十万规模的服务器上的运维服务,所提供的服务包括服务管理、资源定位、监控、部署、分布式任务调度等等。最近几年,团队着力于发展智能化运维能力以及AIOps产品化建设。 众所周知,百度除了搜索业务之外,还有很多其他的业务线,有像地图、百科、知道、网盘这样的老牌业务,也有诸如像教育、医疗这样的新兴业务,每个业务在规模上、服务架构上都有很大差异。业务本身对稳定性的要求很高,需要保持99.995%的高可用,同时在业务上云的背景下,虚拟化、混合云等都给我们带来了新的挑战。 百度运维经历了从脚本&工具、基础运维平台、开放可定制运维平台到我们现在的智能运维平台,这样四个阶段的转变。过去运维的核心目标是提升效果,比如持续交付的速度、服务稳定性、运营成本等。经过这么多年的建设,整个运维行业已经非常成熟,而我们所支撑业务规模仍在不断增长,越来越多的运维场景和问题无法用传统方法来解决,而运维效率也难以继续支撑业务规模的快速扩张,所以我们更加关注怎么样解放运维自身的效率,以及解决传统运维方法(人工、自动化)所解决不了的问题。 这就好比从马车到汽车是为了提升运输效率,而到汽车已经接近饱和的时候,我们又希望用自动驾驶把驾驶员从开车这项体力劳动中解放出来,不仅可以增加运行效率,同时也可以减少交通事故率,这也是我们对智能运维的诉求。 发展:AIOps,从理念到落地 2016年Gartner报告中提出了AIOps概念,也就是Algorithmic IT Operations;基于算法的IT运维,主要指用大数据、机器学习驱动自动化、服务台、监控这些场景下的能力提升。 我们从2014年开始做智能运维方面的探索,最开始也是集中在监控指标分析、报警分析、故障根因分析、性能和成本分析这些方面,到2016年我们已经完成将AI应用于完整的运维平台研发的论证。在我们语义下的AIOps,目标是将人的知识和运维经验与大数据、机器学习技术相结合,开发成一系列的智能策略,融入到运维系统中。用这样的智能运维系统去完成运维任务,是我们所认为的AIOps,也就是Artificial Intelligence IT Operations。有意思的是,2017年之后的Gartner报告也将AIOps的概念改成了Artificial Intelligence IT Operations。 我们认为AIOps中有三部分不可或缺,一个是运维开发框架,这个是我们后续智能运维研发的骨架,第二个是运维知识库,这是让骨架能与我们真实线上环境关联起来的关键因素,起到了血肉的作用,让骨架能动起来。而最后一个则是运维策略库,这是运维的大脑,控制着运维平台的行为。 使用运维开发框架实现的运维程序,我们称其为运维机器人。运维机器人可以在多种不同的运维场景下提供多样的运维能力,服务不同类型的业务和用户。 框架:新的运维开发模式 运维开发框架基于这样一个抽象,就是如果我们把线上环境看做一个黑盒服务,那么我们对它的操作无非读写两类,所谓的写也就是操作控制流,是那种要对线上状态做一些改变的操作,我们常说的部署、执行命令,都属于这一类;另一类是读,指的是数据流,也就是要从线上获取状态数据,并进行一些聚合统计之类的处理,我们常说的指标汇聚、异常检测、报警都在这个里面。通过运维知识库,可以在这两种操作的基础上,封装出多种不同的运维机器人,对业务提供高效率、高质量以及高可用方面的能力。 根据操作流和数据流的不同,我们把框架分成了两部分,最基础的是运维执行框架,在这之上,加上分布式计算组件的支持,我们还建设了用于运维大数据计算的计算框架。 1工程化 运维开发框架给开发者提供一系列的开发套件,除了包含了一系列的基础能力,还包含了一个标准的运维工程研发流程。 在过去,运维研发采用简单的开发-使用方式,缺少必要的测试维护。而现在,在代码开发阶段,可以通过执行框架,用统一的操作接口库提升研发效率。在测试阶段,开发套件提供了单测和仿真系统,简化测试环境搭建。在上线后的阶段,通过状态服务和托管系统,可满足在各灾难场景下的运维机器人的自维护。 2组件化 运维开发框架通过三种不同的组件功能组合成运维机器人。分别是感知器、决策器和执行器。这三种组件针对各自使用场景,提供了多种架构能力。 感知器运维机器人的眼睛和耳朵感,就像人有两个眼睛和两个耳朵一样。运维机器人也可以挂载多个感知器来获取不同事件源的消息,比如监控的指标数据或者是报警事件,变更事件这些,甚至可以是一个定时器。这些消息可以以推拉两种方式被感知器获取到。这些消息也可以做一定的聚合,达到阈值再触发后续处理。 决策器是运维机器人的大脑,所以为了保证决策的唯一,机器人有且只能有一个决策器。决策器也是使用者主要要扩展实现的部分。除了常见的逻辑判断规则之外,未来我们还会加入决策树等模型,让运维机器人自主控制决策路径。 执行器是运维机器人的手脚,所以同样的,执行器可以并行的执行多个不同的任务。执行器将运维长流程抽象成状态机和工作流两种模式。这样框架就可以记住当前的执行状态,如果运维机器人发生了故障迁移,还可以按照已经执行的状态让长流程断点续起。 知识库:运维的知识图谱 知识库是智能运维架构中非常重要的一部分:所有要处理的数据都来自知识库,以及所有处理后的数据也都会再进入到知识库中。知识库由三部分组成,分别是元数据、状态数据和事件数据。持续的数据建设,是智能运维建设的关键。 考虑到未来需要对接不同的内部云平台和公有云平台,所以我们的运维数据也需要从底层的多种不同的运维平台中抽取,清洗和做数据的整合。并以尽可能高的时效性提供给平台用户使用。因此我们知识库建设遵照这四个能力指标进行,分别是全、准、新、稳。 由于知识库涉及的存储的内容篇幅太大,并且是相对独立的一块工作,所以这里就不再展开了。 实践:运维机器人 单机房故障自愈是2017年我们完成的重点项目,目标是将单机房范围的故障自愈水平普遍提升到L4级(整个处理过程,包括决策过程基本无人介入)。当然,另一部分原因是过去一两年发生的几次业界重大线上事故,我们希望可以防微杜渐,进一步提升MTTR水平。 相比较原有的单机房故障处理方式,在感知、决策、执行三个方面,L4级的单机房故障自愈系统效果显著: 1.感知方面,智能异常检测算法替代过去大量误报漏报的阈值检测方法; 2.决策方面,具备全局信息、自动决策的算法组件替代了过去“老中医会诊”的人工决策模式; 3.执行方面,状态机等执行长流程组件的加入,让执行过程可定位、可复用。 目前L4级的单机房故障自愈,已经覆盖百度大多数核心业务线,止损效率可做到分钟级,最快秒级止损,较人工止损效率提升60%-99%。 总结 随着AIOps逐渐走向成熟和产品化,必将有越来越多的运维场景被AIOps所变革,而我们,百度云智能运维团队,也希望秉承着这个方向,为行业贡献更多的创新理念、技术和产品,欢迎大家一起加入探讨。 最后,用一句话来总结下工程架构对于智能运维的意义: 框架在手,AI我有:智能时代,框架会越来越重要,从机器学习框架TensorFlow到自动驾驶框架Apollo,概莫能外。 原文链接地址: https://developer.baidu.com/topic/show/290063
来源:OSCHINA
发布时间:2019-03-30 11:52:00
UK8S是UCloud推出的Kubernetes容器云产品,完全兼容原生API,为用户提供一站式云上Kubernetes服务。我们团队自研了CNI(Container Network Interface)网络插件,深度集成VPC,使UK8S容器应用拥有与云主机间等同的网络性能(目前最高可达10Gb/s, 100万pps),并打通容器和物理云/托管云的网络。过程中,我们解决了开源kubelet创建多余Sandbox容器导致Pod IP莫名消失的问题,确保CNI插件正常运行,并准备将修复后的kubelet源码提交给社区。 深度集成VPC的网络方案 按照我们的设想,开发者可以在UK8S上部署、管理、扩展容器化应用,无需关心Kubernetes集群自身的搭建及维护等运维类工作。UK8S完全兼容原生的Kubernetes API, 以UCloud 公有云资源为基础, 通过自研的插件整合打通了ULB、UDisk、EIP等公有云网络和存储产品,为用户提供一站式云上Kubernetes服务。 其中VPC既保障网络隔离,又提供灵活的IP地址定义等,是用户对网络的必备需求之一。UK8S研发团队经过考察后认为,UCloud基础网络平台具有原生、强大的底层网络控制能力,令我们能抛开Overlay方案,把VPC的能力上移到容器这一层,通过VPC的能力去实现控制和转发。 UK8S每创建一个Pod都为其申请一个VPC IP并通过VethPair配置到Pod上,再配置策略路由。 原理如下图所示。 此方案具有以下优势: 无Overlay,网络性能高。50台Node下的测试数据表明,容器与容器之间的网络性能,相对于云主机与云主机之间,只有轻微差异(小包场景下,pps 会有 3~5% 损耗),而且Pod网络性能各项指标(吞吐量,包量,延迟等)不会随着节点规模增大而削减。而Flannel UDP,VXLan模式和Calico IPIP的模式存在明显的性能消耗。 Pod能直通公有云和物理云。对于使用公有云和物理云的用户而言,业务上K8S少了一层障碍,多了一份便利。而Flannel的host gw模式下,容器无法访问公有云和物理云主机。 而CNI的工作流程如下所示。 创建Pod网络过程: 删除Pod网络过程: Pod IP 消失问题的排查与解决 为了测试CNI插件的稳定性,测试同学在UK8S上部署了一个CronJob,每分钟运行一个Job任务,一天要运行1440个任务。该CronJob定义如下: apiVersion: batch/v1beta1 kind: CronJob metadata: name: hello spec: schedule: "*/1 * * * *" jobTemplate: spec: template: spec: containers: - name: hello image: busybox args: - /bin/sh - -c - date; echo Hello from the Kubernetes cluster restartPolicy: OnFailure 每运行一次Job都要创建一个Pod, 每创建一个Pod,CNI插件需要申请一次VPC IP,当Pod被销毁时,CNI插件需要释放该VPC IP。 因此理论上,通过该CronJob每天需要进行1440次申请VPC IP和释放VPC IP操作。 然而,经过数天的测试统计,发现通过该CronJob,集群每天申请IP次数高达2500以上, 而释放的的IP次数也达到了1800。申请和释放次数都超过了1440,而且申请次数超过了释放次数,意味着,部分分配给Pod的VPC IP被无效占用而消失了。 CNI:待删除的IP去哪儿了? 仔细分析CNI插件的运行日志,很快发现,CNI在执行拆除SandBox网络动作(CNI_COMMAND=DEL)中,存在不少无法找到Pod IP的情况。由于UK8S 自研的CNI查找Pod IP依赖正确的Pod网络名称空间路径(格式:/proc/10001/net/ns),而kubelet传给CNI的NETNS环境变量参数为空字符串,因此,CNI无法获取待释放的VPC IP,这是造成IP泄露的直接原因,如下图所示。 问题转移到kubelet, 为什么kubelet会传入一个空的CNI_NETNS环境变量参数给CNI插件? 随后跟踪kubelet的运行日志,发现不少Job Pod创建和销毁的时候,生成了一个额外的Sandbox容器。Sandbox容器是k8s pod中的Infra容器,它是Pod中第一个创建出来的容器,用于创建Pod的网络名称空间和初始化Pod网络,例如调用CNI分配Pod IP,下发策略路由等。它执行一个名为pause的进程,这个进程绝大部分时间处于Sleep状态,对系统资源消耗极低。奇怪的是,当任务容器busybox运行结束后,kubelet为Pod又创建了一个新的Sandbox容器,创建过程中自然又进行了一次CNI ADD调用,再次申请了一次VPC IP。 回到UK8S CNI,我们再次分析重现案例日志。这一次有了更进一步的发现,所有kubelet传递给NETNS参数为空字符串的情形都发生在kubelet试图销毁Pod中第二个Sandbox的过程中。反之,kubelet试图销毁第二个Sandbox时,给CNI传入的NETNS参数也全部为空字符串。 到这里,思路似乎清晰了不少,所有泄露的VPC IP都是来自第二个Sandbox容器。因此,我们需要查清楚两个问题: 为什么会出现第二个Sandbox容器? 为什么kubelet在销毁第二个Sandbox容器时,给CNI传入了不正确的NETNS参数? 第二个Sandbox:我为何而生? 在了解的第二个Sandbox的前世今生之前,需要先交待一下kubelet运行的基本原理和流程。 kubelet是kubernetes集群中Node节点的工作进程。当一个Pod被kube-sheduler成功调度到Node节点上后, kubelet负责将这个Pod创建出来,并把它所定义的各个容器启动起来。kubelet也是按照控制器模式工作的,它的工作核心是一个控制循环,源码中称之为syncLoop,这个循环关注并处理以下事件: Pod更新事件,源自API Server; Pod生命周期(PLEG)变化, 源自Pod本身容器状态变化, 例如容器的创建,开始运行,和结束运行; kubelet本身设置的周期同步(Sync)任务; Pod存活探测(LivenessProbe)失败事件; 定时的清理事件(HouseKeeping)。 在上文描述的CronJob任务中, 每次运行Job任务都会创建一个Pod。这个Pod的生命周期中,理想情况下,需要经历以下重要事件: Pod被成功调度到某个工作节点,节点上的Kubelet通过Watch APIServer感知到创建Pod事件,开始创建Pod流程; kubelet为Pod创建Sandbox容器,用于创建Pod网络名称空间和调用CNI插件初始化Pod网络,Sandbox容器启动后,会触发第一次kubelet PLEG(Pod Life Event Generator)事件。 主容器创建并启动,触发第二次PLEG事件。 主容器date命令运行结束,容器终止,触发第三次PLEG事件。 kubelet杀死Pod中残余的Sandbox容器。 Sandbox容器被杀死,触发第四次PLEG事件。 其中3和4由于时间间隔短暂,可能被归并到同一次PLEG事件(kubelet每隔1s进行一次PLEG事件更新)。 然而,在我们观察到的所有VPC IP泄露的情况中,过程6之后“意外地”创建了Pod的第二个Sandbox容器,如下图右下角所示。在我们对Kubernetes的认知中,这不应该发生。 对kubelet源码(1.13.1)抽丝剥茧 前文提到,syncLoop循环会监听PLEG事件变化并处理之。而PLEG事件,则来源kubelet内部的一个pleg relist定时任务。kubelet每隔一秒钟执行一次relist操作,及时获取容器的创建,启动,容器,删除事件。 relist的主要责任是通过CRI来获取Pod中所有容器的实时状态,这里的容器被区分成两大类:Sandbox容器和非Sandbox容器,kubelet通过给容器打不同的label来识别之。CRI是一个统一的容器操作gRPC接口,kubelet对容器的操作,都要通过CRI请求来完成,而Docker,Rkt等容器项目则负责实现各自的CRI实现,Docker的实现即为dockershim,dockershim负责将收到的CRI请求提取出来,翻译成Docker API发给Docker Daemon。 relist通过CRI请求更新到Pod中Sandbox容器和非Sandbox容器最新状态,然后将状态信息写入kubelet的缓存podCache中,如果有容器状态发生变化,则通过pleg channel通知到syncLoop循环。对于单个pod,podCache分配了两个数组,分别用于保存Sandbox容器和非Sandbox容器的最新状态。 syncLoop收到pleg channel传来事件后,进入相应的sync同步处理流程。对于PLEG事件来说,对应的处理函数是HandlePodSyncs。这个函数开启一个新的pod worker goroutine,获取pod最新的podCache信息,然后进入真正的同步操作:syncPod函数。 syncPod将podCache中的pod最新状态信息(podStatus)转化成Kubernetes API PodStatus结构。这里值得一提的是,syncPod会通过podCache里各个容器的状态,来计算出Pod的状态(getPhase函数),比如Running,Failed或者Completed。然后进入Pod容器运行时同步操作:SyncPod函数,即将当前的各个容器状态与Pod API定义的SPEC期望状态做同步。下面源码流程图可以总结上述流程。 SyncPod:我做错了什么? SyncPod首先计算Pod中所有容器的当前状态与该Pod API期望状态做对比同步。这一对比同步分为两个部分: 检查podCache中的Sandbox容器的状态是否满足此条件:Pod中有且只有一个Sandbox容器,并且该容器处于运行状态,拥有IP。如不满足,则认为该Pod需要重建Sandbox容器。如果需要重建Sandbox容器,Pod内所有容器都需要销毁并重建。 检查podCache中非Sandbox容器的运行状态,保证这些容器处于Pod API Spec期望状态。例如,如果发现有容器主进程退出且返回码不为0,则根据Pod API Spec中的RestartPolicy来决定是否重建该容器。 回顾前面提到的关键线索:所有的VPC IP泄露事件,都源于一个意料之外的Sandbox容器,被泄露的IP即为此Sandbox容器的IP。刚才提到,SyncPod函数中会对Pod是否需要重建Sandbox容器进行判定,这个意外的第二个Sandbox容器是否和这次判定有关呢? 凭kubelet的运行日志无法证实该猜测,必须修改源码增加日志输出。重新编译kubelet后,发现第二个Sandbox容器确实来自SyncPod函数中的判定结果。进一步确认的是,该SyncPod调用是由第一个Sandbox容器被kubelet所杀而导致的PLEG触发的。 那为什么SyncPod在第一个Sandbox容器被销毁后认为Pod需要重建Sandbox容器呢?进入判定函数podSandboxChanged仔细分析。 podSandboxChanged获取了podCache中Sandbox容器结构体实例,发现第一个Sandbox已经被销毁,处于NOT READY状态,于是认为pod中已无可用的Sandbox容器,需要重建之,源码如下图所示。 注意本文前面我们定位的CronJob yaml配置, Job模板里的restartPolicy被设置成了OnFailure。SyncPod完成Sandbox容器状态检查判定后,认为该Pod需要重建Sandbox容器,再次检查Pod的restartPolicy为OnFailure后,决定重建Sandbox容器,对应源码如下。 可以看出kubelet在第一个Sandbox容器死亡后触发的SyncPod操作中,只是简单地发现唯一的Sandbox容器处于NOT READY状态,便认为Pod需要重建Sandbox,忽视了Job的主容器已经成功结束的事实。 事实上,在前面syncPod函数中通过podCache计算API PodStatus Phase的过程中,kubelet已经知道该Pod处于Completed状态并存入apiPodStatus变量中作为参数传递给SyncPod函数。如下图所示。 Job已经进入Completed状态,此时不应该重建Sandbox容器。而SyncPod函数在判定Sandbox是否需要重建时, 并没有参考调用者syncPod传入的apiPodStatus参数,甚至这个参数是被忽视的。 第二个Sandbox容器的来源已经水落石出,解决办法也非常简单,即kubelet不为已经Completed的Pod创建Sandbox,具体代码如下所示。 重新编译kubelet并更新后,VPC IP泄露的问题得到解决。 下图可以总结上面描述的第二个Sandbox容器诞生的原因。 事情离真相大白还有一段距离。还有一个问题需要回答: 为什么kubelet在删除第二个Sandbox容器的时候, 调用CNI拆除容器网络时,传入了不正确的NETNS环境变量参数? 失去的NETNS 还记得前面介绍kubelet工作核心循环syncLoop的时候,里面提到的定期清理事件(HouseKeeping)吗?HouseKeeping是一个每隔2s运行一次的定时任务,负责扫描清理孤儿Pod,删除其残余的Volume目录并停止该Pod所属的Pod worker goroutine。HouseKeeping发现Job Pod进入Completed状态后,会查找该Pod是否还有正在运行的残余容器,如有则请理之。由于第二个Sandbox容器依然在运行,因此HouseKeeping会将其清理,其中的一个步骤是清理该Pod所属的cgroup,杀死该group中的所有进程,这样第二个Sandbox容器里的pause进程被杀,容器退出。 已经死亡的第二个Sandbox容器会被kubelet里的垃圾回收循环接管,它将被彻底停止销毁。然而由于之前的Housekeeping操作已经销毁了该容器的cgroup, 网络名称空间不复存在,因此在调用CNI插件拆除Sandbox网络时,kubelet无法获得正确的NETNS参数传给CNI,只能传入空字符串。 到此,问题的原因已经确认。 问题解决 一切水落石出后,我们开始着手解决问题。为了能确保找到所删除的Pod对应的VPC IP,CNI需要在ADD操作成功后,将PodName,Sandbox容器ID,NameSpace,VPC IP等对应关联信息进行额外存储。这样当进入DEL操作后,只需要通过kubelet传入的PodName,Sandbox容器ID和NameSpace即可找到VPC IP,然后通过UCloud 公有云相关API删除之,无需依赖NETNS操作。 考虑到问题的根因是出现在kubelet源码中的SyncPod函数,UK8S团队也已修复kubelet相关源码并准备提交patch给Kubernetes社区。 写在最后 Kubernetes依然是一个高速迭代中的开源项目,生产环境中会不可用避免遇见一些异常现象。UK8S研发团队在学习理解Kubernetes各个组件运行原理的同时,积极根据现网异常现象深入源码逐步探索出问题根因,进一步保障UK8S服务的稳定性和可靠性,提升产品体验。 2019年内UK8S还将支持节点弹性伸缩(Cluster AutoScaler)、物理机资源、GPU资源、混合云和ServiceMesh等一系列特性,敬请期待。 欢迎扫描下方二维码,加入UCloud K8S技术交流群,和我们共同探讨Kubernetes前沿技术。 如显示群人数已加满,可添加群主微信zhaoqi628543,备注K8S即可邀请入群。
来源:OSCHINA
发布时间:2019-04-12 16:53:00
行业内接入网络去堆叠已经逐步成为主流方向,在大型互联网公司也已经批量部署。但由于京东集团不同的业务需求及历史原因,没有条件完全复制目前主流的ARP转主机路由方式的去堆叠方案,这促使我们设计一种尽可能满足各类业务需求的方案。 近几年来云市场迅速发展,越来越多的用户把关键应用甚至全部应用部署到公有云上。云服务商在产品收入快速增长同时,也深刻体会到产品的高可用性对用户发展和用户留存的重要性。面向用户的产品SLA的实现效果取决于其依赖的各个环节,而基础网络是所有产品需要依赖的一个重要环节,因此,提升网络高可用SLA对整体提升产品整体SLA有着重要促进作用。今天我们要谈的异构去堆叠就是京东云在提高网络高可用SLA方面的新技术研究。用户自有网络其实也面临同样的问题,有提高网络可靠性需求的用户可以考虑在自有网络中使用类似方案。 网络高可用通用解决方案 首先,让我们先来看一下为了实现网络高可用,当下的四种服务器连接主流方案: 由上图可以看出,双网卡/交换机堆叠和双网卡/去交换机堆叠提供了更好的高可用保证。 通用堆叠方式 V.S 通用去堆叠方式 01 堆叠方案 优势 服务器双网卡捆绑,无需特别改造 交换机控制面统一,支持服务器BGP路由方式接入 劣势 多设备统一控制面,可靠性低 升级困难 横向连接浪费端口 02 去堆叠方案 常见的去堆叠方案有以下两种: 去堆叠方案- 路由方式 优势 交换机完全独立 支持异构 路由方式接入的服务器无需特别改造 劣势 2层方式接入的服务器需要进行改造 服务器除网卡IP外需配置单独业务IP 服务器多IP增加业务方运维复杂度 静态路由方式不适合VM或Docker漂移 动态路由方式要求全部服务器运行路由协议 2.去堆叠方案- ARP转主机路由方式 优势 交换机完全独立 服务器网卡捆绑方式不变,便于运维 服务器网卡捆绑方式不变,便于运维 劣势 一组交换机要求同厂商,不支持异构 需要修改服务器Linux内核ARP处理部分 不支持服务器BGP路由方式接入 通过以上对比,可以发现堆叠与去堆叠方式其实都可以实现网络的高可用,但各有利弊。针对这样的情况,京东云提出了一种理想的去堆叠方式,可以满足下图中的所有需求。 异构去堆叠方案 01 实现方法 交换机侧 IP配置/24掩码,两台交换机可配置相同IP或者不同IP; 启用ARP代理及转主机路由功能; 配置ARP超时时间为5s以便在服务器不响应ARP时快速撤回主机路由。 服务器侧 服务器双网卡配置相同IP地址(掩码/32); 采用onlink方式配置目的为交换机IP的静态路由指向对应网卡; 采用onlink方式配置缺省路由同时指向两块网卡; 需要运行Ifplugd进程监控物理连接,物理连接发生UP/DOWN时执行相应ARP Ping和路由修改操作。 02 冗余测试 从上图中的测试拓扑和结果中可以看出,不论是软件操作上的禁用还是硬件拔出,在设定的收敛时间内,服务器的网络一直保持高可用。 03 异构去堆叠方案小结 异构去堆叠方案优势 交换机完全独立 异构避免单一厂商风险 异构推动自研交换机快速上线 服务器侧支持2层方式或BGP路由方式 不修改Linux内核 注意事项 需要内核版本支持L4 HASH方式ECMP(Centos 7.4以上) 需要运行Ifplugd进程监控物理连接 总结 综上所述,异构去堆叠有助于实现苛刻的网络SLA和极致的用户体验。京东云在设计去堆叠方案时首先考虑了异构,一方面因为单一厂商对网络高可用SLA还是一个重要风险点,另一方面异构方式可以促进京东正在进行的自研交换机实现快速部署。众所周知,自研交换机在各大互联网公司都是重点项目,但软件和硬件的稳定性一直是批量部署的重大障碍。而异构去堆叠利用自研和商用交换机1+1的方式可以大大降低自研交换机稳定性的影响。 ·END·
来源:OSCHINA
发布时间:2019-04-11 11:36:00
今天, Google Cloud NEXT 2019大会召开,在这场规模三万人的盛会上,Google宣布推出Anthos作为多云服务新方案,提供跨云(目前仅支持AWS和Azure)管理Kubernetes集群。 Anthos(前身为Cloud Service Platform)让用户可以跨环境构建和管理现代混合应用程序,用户可以在Anthos之上交付和管理容器、Kubernetes、微服务架构和Service Mesh等。据Google CEO Sundar Pichai在大会发布时的介绍,Google自身作为公有云提供商,产品Anthos平台甚至可以支持部分竞争对手的云,如目前支持AWS和Azure,但除这二者之外的其他公有云如阿里云、Oracle、IBM等的云都尚不支持。 技术前瞻性和持续创新得到证明 Anthos的发布,对Rancher来说是一则十足的好消息。在Google Anthos中,我们看到了它与Rancher的愿景的完美契合。因为Rancher始终相信Kubernetes将成为所有公有云和私有云都会提供的、标准化的基础架构,企业Kubernetes平台必须提供多集群、多云管理的功能。 早在两年前,Rancher就预见到企业将需要跨不同的公有云和私有数据中心管理多个Kubernetes集群。正是怀着这种理念,我们构建了Rancher 2.0,它可以与所有Kubernetes集群协同工作,包括Google GKE、Amazon EKS、Azure AKS、华为云、阿里云、腾讯云等等,是业界第一个、也是目前唯一一个与所有主流公有云厂商完成官方合作及对接的Kubernetes平台。 在过去的12个月里,已有成千上万的具有前瞻性思维的企业在生产环境中应用了Rancher 2.0。但是在初期,当我们向用户阐释Rancher能够管理多Kubernetes服务(如GKE、AKS、EKS等)的这一功能的优势时,有用户虽然对这一创新特性兴奋与好奇,却仍忍不住问我们,“管理多Kubernetes集群与服务?Rancher是对的吗?”如今,Google正式推出Anthos足以强力地证明,Rancher对多集群多云Kubernetes的坚信和前瞻性,是对的。 Container和Kubernetes技术均正处于采用的早期阶段。我们相信创新的产品和服务是实现大规模采用的关键。我们喜欢Google Anthos和Google Cloud Run(同样是今天全新发布的)等新服务,同时也为Rancher团队开发的创新技术感到自豪。Rancher 2.0是业界第一个多集群、多云Kubernetes管理平台。除此之外,Rancher Labs的创新步伐从未放缓,如最近推出的轻量级Kubernetes发行版K3s、多Kubernetes集群网络框架Submariner、对单一应用程序跨多Kubernetes集群的部署与管理等等,都广受客户和用户称赞。 Anthos:竞争者?推动者? 那Google会成为Rancher的竞争对手吗?答案是否定的。首先,Rancher是一个开源的平台,而Anthos是一种云服务。我们甚至相信,Anthos的发布将毫无疑问加速Rancher被更大规模地采用。正如Google发布GKE而极大地帮助普及了Kubernetes技术一样,我们也相信Anthos将促进将多集群、多云Kubernetes管理带入更主流的阶段。 其次,作为一个完全开源的Kubernetes管理平台,Rancher自身并非公有云提供商,因而一如既往的是所有公有云提供商、包括他们提供的托管Kubernetes服务的合作伙伴而非竞争对手。Rancher也才更能中立地提供对其他厂商Kubernetes服务的最一流支持。因为公有云厂商之间天然的竞争关系,我们很难想象AWS乐意致力于把运行在EKS上的应用无缝迁移到Google GKE或者其他云服务商提供的Kubernetes服务上,反之亦然。 从世界范围内包含Google GKE、Amazon EKS、Azure AKS、华为云、阿里云、腾讯云以及正在加入Rancher合作伙伴列表的众多主流公有云与Rancher一直以来的紧密合作中可见,Rancher也始终是所有第三方公有云厂商普遍最为信任的统一管理平台。 宠物与牛的故事 听过那个云计算时代“宠物”与“牛”的故事吗? 传统的物理机时代,物理机(及它所代表的计算资源)被工程师称为“宠物”,它们价格高昂,需要精心维护和保养。而在云计算时代,工程师可以把服务器当成牛,一头牛倒下没关系,只要放牧人活着就行,服务器价格便宜,就像一次性工具,即时它们倒下,一台设备上的任务也能转移到另一台,对公司业务不会造成任何影响。 我们将公司及产品命名为“Rancher(放牧人)”,就是因为我们愿景中的未来里,企业可以自由选择并搭配着从他们喜欢的云提供商那里获得计算资源,而Rancher会开发一个软件平台来完美实现这种统一体验。随着Anthos的发布,我们相信整个行业又向实现这一愿景迈进了一步! 本文基于Rancher Labs联合创始人及CEO梁胜所写博客删改编辑而成: https://rancher.com/blog/2019/era-of-multi-cluster-multi-cloud-kubernetes-has-arrived/
来源:OSCHINA
发布时间:2019-04-11 09:20:00
问题演变过程 时间点1:高防+WAF+SLB+2台ECS 时间点2:高防+WAF+SLB+4台ECS 问题描述 在时间点1时,没有发现明显的负载不均衡的情况。在时间点2时,出现大部分请求都打到了其中一台ECS上。需要定位问题原因 问题梳理 问题链路 是SLB后端的ECS出现负载不均衡的请求,那么直接影响这个转发算法的,是WAF以及SLB。那么和高防没有关系了。 配置情况 SLB:TCP监听,WRR转发算法,开启会话保持 WAF:无特殊配置,域名直接回源负载均衡IP 问题点1:轮询算法+会话保持 措施: 尝试修改轮询算法为WLC,会话保持时间调短。 然而这个优化措施效果并不明显,由于开启了会话保持,那原有负载不均衡的情况下,调整WRR算法到WLC的算法,没有实现预期的WLC。 但是从另外一个角度来说,如果源IP非常分散的场景下,即使有会话保持,理论上还是应该在经过一个较长的时间段之后,依然能够到达均衡。 这里由于是使用WAF的回源地址进行访问,所以对负载均衡来说,客户端的公网IP地址是固定的,一直是固定的几个;从而调整WLC+会话保持的调整收效甚微。 问题点2:会话保持模板刷新问题 措施: 尝试关闭会话保持。 稍有成效: 关闭会话保持后,经过一段时间的通信,4台ECS初步的开始均衡,但是到了一个固定值之后;没有继续均衡,一直保持着1:2的状态。 这里有 2个知识点 : 1、WLC算法的计数开始是从调整为这个算法的时间点开始的;那么如果历史开始就出现不均衡,那么开启后还是会不均衡的。 2、由于WAF的回源地址与SLB的通信一直在,没有断过所以历史的会话保持的效果依然存在,已经会话保持的IP,依然会发给对应负载均衡的RS,导致不均衡。 推荐的解法为: 使用负载均衡的权重功能,将连接数多的机器的权重调低,待4台机器的连接数基本均衡后,将RS的权重都调整为一致。 作者:枫凡 原文链接 ​ 本文为云栖社区原创内容,未经允许不得转载。
来源:OSCHINA
发布时间:2019-05-14 12:11:00
【ZStack学堂】第二季第5期报名开始啦!前几期的分享收获了很多小伙伴的好评,第5期课程将与大家一起分享ZStack GPU透传、IOMMU/ VT-d简介、典型场景:3D渲染等内容。欢迎扫描海报二维码报名参与, 转发海报或参与直播间互动还有机会获取ZStack精心准备的小礼品哦!
来源:OSCHINA
发布时间:2019-05-14 09:19:00
作者:ZStack 社区 王彬 Iaas云服务的普及,让我们在使用服务器的时候享受了飞一般的感觉,新兴企业在构建自己的系统时,往往都会选择购买云厂商的云服务器(虚拟机)进行使用,使用这样的虚拟机企业不需要购置任何硬件,不需要考虑机房、网络、服务器维护相关的工作便可以获取到一个低成本、安全免维护、伸缩性强、可灵活迁移的云服务器。在这个云服务器上我们可以快速的构建企业的业务系统。 随着企业的不断发展,使用公有云服务在虚拟机的数量到达一个数量级的时候,成本会高于私有云,且虚拟机数量越大这个成本差就会越大,大多数互联网企业在创业初期都是使用主流的公有云服务,但是随着企业内部业务量不断上升虚拟机数量也不断上升,相关的成本也在一直增加。并且随着业务类型越来越多时,业务的安全需求也受到越来越多的挑战,此时就需要考虑自建机房、自建私有云,将公有云的服务迁移到私有云上降低成本并进行内部的安全控制。 关于私有云的构建,目前业界有很多的解决方案。我也有多方了解和比较。前段时间接触到了ZStack云平台,为了更好的理解和掌握ZStack的产品,特地前往ZStack进行参观学习。 ZStack(上海云轴)成立于2015年,是一家坚持自主创新、专注于产品化的云计算公司。 在本次学习过程中,我了解到 ZStack有一个极大的亮点——操作简单 。使用ZStack的时候你不需要考虑什么SSO、数据库、控制服务部署调试,也不需要考虑什么Nova、Swift、Glance、Keystone等等。你所要的在ZStack的镜像中已经完整的进行了配置。 下载ISO ZStack-x86_64-DVD-3.3.0-c74.iso 安装ZStack系统,简单点说就是装个LINUX,和安装CentOS一样,系统装好后就完成了一个私有云云平台的安装。 2. 平台初始化 3.添加镜像 4.创建虚机 5.安装Windows系统虚拟机并加载驱动 通过以上的方式,我们可以快速的构建一个私有云环境并部署一个Windows2012的虚拟机,接着就可以运行业务了。 云计算平台承载用户核心业务,其系统架构设计必须稳定且高效。现阶段一些大的集群或者云平台扩容或上线的话需要特别大的资源占用和极高硬件成本。另外,在部署时需要购买更高级的一些License授权才可以使用。然而ZStack不需要很高配置的硬件和授权,也可以支持两千多个节点的部署。现在已经有很多客户在使用这个平台了,比如饿了么等一些数据量访问很大的公司。ZStack的扩容性、扩展性很强,可以在不停机的情况下实现在线的、无缝的升级和扩展一些其他的常用功能。 重启zstack平台不影响业务 云计算基础平台小到3~5服务器,大到数万台,资源管理平台必须有强的适配性。以订餐平台为例,基本上有两个业务高峰期,在这两业务高峰期的时候人员的访问会急剧增加,使用的普通服务器或者普通云平台的部署来支持的话,会造成大量的资源浪费,这时,ZStack的弹性伸缩功能就派上用场了。它可以设定一系列的计划,当某个时间段的时候,企业的虚拟云主机数量不够用,它可以按照弹性伸缩的计划自动扩展,包括添加一些可以支持高并发的访问操作等,在几分钟之内就可以出现几百台所需的虚拟云主机。 另外, ZStack具有良好的兼容性,不会有硬件捆绑情况,可将现有设备进行利旧统一成资源池。减轻企业采购压力,促进节能环保。 以上就是我在使用ZStack过程的心得体会。 根据IDC的数据,到2021年,中国IT市场,公有云占比约27%,私有云占比约26%,剩下的45.8%是没有云化的传统IT,中国私有云市场近几年的增速会超过公有云。Gartner明确说中国未来将是市场上最大的私有云市场之一。 因此,中国私有云的市场前景非常广阔,私有云市场与公有云市场将会产生非常大的交集,这就是混合云市场。 日后,我司将持续使用ZStack云管平台,完成客户从公有云到私有云部署的全过程。
来源:OSCHINA
发布时间:2019-05-10 17:53:00
6月20日,北京,由Rancher Labs主办的【2019企业容器创新大会】限免报名已开启!全天18场演讲,特邀中国人寿、中国联通、平安科技、新东方、阿里云、百度云等著名企业的IT负责人,分享容器技术的企业级落地经验。 大会上,Rancher Labs研发经理还将现场Demo即将发布的Rancher 2.3中Istio、Windows容器的功能及使用!还有K3s、Rio等的现场交流。点击 http://hdxu.cn/hMsQ8 了解详情及在线报名啦~ 2019年6月6日,Rancher Labs发布了Rancher全新版本2.2.4,该版本修复了近期发现的两个安全漏洞CVE-2019-12303 和 CVE-2019-12274,项目级别的监控功能也在此版本回归,还有一系列功能与优化。 CVE修复 2.2.4版本包含两个安全漏洞修复 CVE-2019-12303 和 CVE-2019-12274 第一个漏洞会影响v2.0.0到v2.2.3的版本,项目管理员可以在对接日志系统时,通过注入额外的Fluentd配置参数,来读取到Fluentd容器内的文件或执行任意命令,例如其他项目管理员配置的ElasticSearch。 具体链接: https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2019-12274 第二个漏洞会影响v1.6.0到v1.6.27(使用Cattle和Kubernetes的用户)以及v2.0.0到v2.2.3的版本。某些内嵌的主机驱动可以配置一个文件路径的参数。通过主机驱动创建主机时,创建者可以通过该漏洞获取Rancher Server内任意容器的内容,例如 /root/.kube/config 。这样,主机创建者可以获取Rancher管理平面的访问权限。 具体链接: https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2019-12303 功能与优化 项目级别监控回归 AKS集群新增多个区域的支持 RKE AWS集群新增了多个区域的支持 增强了全局DNS对CloudFlare的支持 内置监控新版本,该版本对性能和稳定性进行了增强。 解决了加载含有大规模Kubernetes资源的集群时,Rancher UI加载延迟很大的问题。 https://github.com/rancher/rancher/issues/18522 通过压缩Rancher服务器与Rancher Agent之前的通讯信息,从而优化了性能。 https://github.com/rancher/rancher/issues/19493 其他优化与增强 下面是2.2.4中修复的一些主要的问题,更多详情可以通过下面的链接查看。 https://github.com/rancher/rancher/milestone/160 增强了应用商店稳定性和性能 https://github.com/rancher/rancher/issues/19881 https://github.com/rancher/rancher/issues/19880 优化了Rancher性能 https://github.com/rancher/rancher/issues/18522 https://github.com/rancher/rancher/issues/16605 https://github.com/rancher/rancher/issues/19934 支持激活OTC主机驱动 普通用户可以创建全局DNS OpenStack集成增强 https://github.com/rancher/rancher/issues/20456 https://github.com/rancher/rancher/issues/20562 etcd快照功能增强 https://github.com/rancher/rancher/issues/18807 https://github.com/rancher/rancher/issues/18793 https://github.com/rancher/rancher/issues/19639 修复了多集群应用模版删除后无法显示的问题 https://github.com/rancher/rancher/issues/20432 修复了HA模式下,Rancher从节点中无法更新应用模版的问题 https://github.com/rancher/rancher/issues/20299
来源:OSCHINA
发布时间:2019-06-11 14:23:04
阿里妹导读:软件质量不是测出来的,但为什么又有这么多测试工程师为了质量而工作?测试是一个成本部门,测试创造的价值是什么?研发的模式在不断地变化,测试的定位如何不断去定义,未来的测试又会是什么形态?今天,阿里巴巴高级测试开发专家傲野总结了对未来测试形态的一些思考,希望对正在做测试的同学有所启发。 前言 从社会发展上来说,各领域的分工越来越细。但从技术部门的发展上来看,测试和开发的角色却是在不断融合,背后的原因是什么?是互联网迭代的速度越来越快促成的多角色融合,还是因为技术(特别是质量技术)先进生产力在逐渐取代落后的生产力? 在回答这些问题之前,我们先来回顾“测试工程师”作为一个职能或者个体在过去的发展历程: 10年前,最初级的测试产出工件是比较一次性的,比如项目中写的文本型测试用例,基本在项目发布后就废弃了。 那个时期测试工作的进阶是方法论,比如能够把测试用例的设计方法,项目流程管理讲得头头是道已经是高阶了。 有一些技术能力的测试同学,投身于自动化脚本的编写。自动化在“软件”测试时代和互联网初期,是真正的硬核能力。 但这样的测试模式和效率都是非常低的,显然无法支撑互联网无快不破的浪潮。2010年以后,在头部企业的测试团队发生了一系列的变革,快速地从上述的这些初级能力,扩大到以 CI/CD 为驱动的技术体系,并最终推动了测试技术产品化进程,形成一个较为清晰的测试平台发展脉络。 在这个将近十年的周期中,由于测试工具、平台的不断创新,测试团队得到了一个突破性的发展。但工具作为传统测试模式的辅助手段,仍然会遇到突破的瓶颈。比如,从全球来看质量也发生了一定的分支: 一种是不断坚持平台化的发展路径:项目质量是基础,不断孵化出各类的效能平台,解决的问题也从传统的质量领域本身,往研发各环节拓展。有些大型的企业也开始沉淀了通用的研发协同平台(研发流水线)。 一种是从内往外突破:比如 Google 的 SRE 团队,以纯技术的手段,打造一个内建且自洽的质量体系(传统以证伪为理论依据的是一个外建的质量体系)。[1] 这两者的方向和目标,是有一定的重合的,比如有些公司以测试负责线下,SRE 负责线上进行区分。但如果从质量这个大的目标来看,未来的成功画面应该是:“质量和效率的结合”和“外建与自洽的结合”。因为只有这样,才能打造一个真正完整的技术质量生态。 实时质量 也是基于上述的一些思考和实践,我们在2017年底提出了“实时质量”的概念。“它不是一个具体的测试技术产品,而是一种面向未来解决质量问题的方法和手段。” 它的主要特性是:运行含测试,实时可反馈。 为什么要往这个方向发展? 随着技术的不断创新和交付模式的不断改变,对于测试团队来说,需要尽快地从交付型质量往实时质量方向进行转移。传统的交付型质量,把测试作为一道道关卡,以任务的方式布防在开发提测、项目发布时。这种方式存在不同角色之间的过多交互,只能起到单点的质量保障。而实时质量的目标是:将质量手段以模块、组件乃至系统化的方式嵌入到业务型应用中,形成实时保障质量的能力。未来开发和测试人员之间的合作(或者就不区分开发测试了),不仅仅是人与人之间的协同,更多是双方分别为完成“业务特性服务的代码”和为完成”业务质量服务的代码“而相互配合,并形成系统级的依赖关系。在提供的这些质量系统上,我们希望公司内部的各种角色都能成为质量的操作者。只在做到这些,我们才可能将测试工作真正从面向过程到面向对象。 图示:理想的测试工作方式 实时质量的架构 要做到质量的实时反馈和面向对象测试,这意味着我们的测试方法和协同方式发生了较为根本性的变化。我们需要以一个合适的方式参与到业务应用中,与此同时我们还需要把测试的各种能力封装成一个个服务,而不是现在的工具。工具终究是需要人来操作的,而我们希望未来测试任务的主体是机器、算法。测试人员只构建测试服务,而不参与测试过程,这也是最符合测试开发 Test Development Engineer 的 job design 。 图示:实时质量架构 那测试到底还需不需要做功能测试?可能在很长一段时间内仍然是需要的,但那一定只是日常工作中很小一部分。 实时质量是基于现有测试能力改造 我们在推进一个新的方向时,尽量不要去推翻重来。如果要面向未来,实时质量必须是可以向下兼容的,因为只是这样才能继承现有的测试沉淀,也才能被团队中的测试人员所接受和支持。只有自己不断进化才符合自然规律。所以我们需要更多强调对现有测试能力的改造,而避免另起炉灶。以下用运营页面测试的实时质量改造作为一个案例。 案例:运营页面的实时质量改造 作为电商域的同学对于运营页面应该非常熟悉,在之前也非常痛恨。比如: “CBU的一次大促,运营人员至少需要配置千级以上的活动页面,而每一个页面上又包含几百上千个商品等活动元素,平均一个页面需要5到10分钟的人肉检测,同时运营和测试人员需要不断就测试标准和 Bug 来回讨论、提交。一次大促下来,我们至少需要十几人/日的测试资源才能保证会场的正确性。” 这个过程很痛苦,运营人员需要不断去找对应的测试同学协同,幸福感很差。而测试人员来说,这些页面的测试更多是一个重复劳动,一个黑盒。能力也得不到什么成长。我们如何对它来进行实时质量的改造呢? 总共分两步: 我们对传统的测试体系进行了改造。把以往通过人工测试的各个测试点,通过自动化的方式来实现。比如基于 DOM 树制定一系列规则,例如403这些的错误都可以被很好地扫描出来。同时,针对于一些无法通过规则排查的问题,我们运用了算法能力。例如空坑检测,一致性检测等。 把以上测试组件,通过消息的方式跟运营页面发布系统对接。 它的系统依赖关系是如下的: 图示:运营页面检测系统依赖图【示意】 同时针对于不同的业务场景,我们开发了不同的页面检测能力,比如针对于 DOM 树的页面检查: 还有基于算法能力的识别能力: 通过上述的改造后,对于运营人员发布页面以及页面的测试就极简化为三步一站式的能力。从以往运营、测试、开发之间的来回交接,变成了运营跟系统之间的交互。不仅提升了运营人员的页面搭建体验,也极大地提升了测试的效率。 在某次运行中活动中实际的执行结果【示意图】: 以上的过程和结果数据,也充分体现了“运行含测试,实时可反馈”的价值。 数据和算法是实时质量的核心 测试出现以来,我们一直习惯于代码逻辑类的测试,但数据一直都是测试很重要的生产材料。因为人肉执行任务的局限性,我们发明了等价类和边界值等测试理论和方法来用尽可能少的成本来尽可能多的验证问题。但一方面算法的不断应用,每一个数据都可能存在个性化的业务表达,我们可能无法找到一个通用的预期结果较验(还是会有一些通用的预期结果的,比如非空判断和区间等,但这类的预期不能很好地做业务判断)。因此,我们也需要用数据和算法能力来武装自己。 在以数据驱动的业务发展进程中,我们的测试主体已经从简单的代码转变为数据+算法。或者说,业务对质量的核心述求,已经从简单的页面错误、代码 BUG 到数据的准确性、算法的有效性(我老板在每次大促前,都要再三叮嘱我数据不能错)。如何来感知质量风险,以及捕获各类的异常?那必须先把数据、流量、监控来做收口,同时提升测试工具在大数据分析上的能力。 基于这些思考,我们构建了全域实时数据校验能力,是一款通过实时获取线上 DB 中的海量业务数据,完成业务数据校验、质量风险感知的产品。 案例:Captain 全域实时数据校验 图示:数据对比框架【示意】 它具备的一些能力: 严格的安全策略。 实时获取线上数据:通过强大的数据支持能力,平台可以在无损线上数据库表的前提下,通过 SQL 查询获取线上 DB 中的真实业务数据,且做到了实时获取,通过数据可以进行完善健壮的数据校验,从根本上提高对于业务的把控。 多样的数据获取方式:目前平台支持多种数据获取方式:单库单表查询、单库多表联表查询、分库分表查询、跨库的多表的联表查询。 多种比对方式支持,比如跨库查询和联表查询等等。 最主要,它可以用一套脚本无损地支持测试环境、灰度、生产环境等。让线下测试的所有经验可以得到复用和沉淀。(我们内部调侃说,这才是带着测试的灵魂的,而其他的很多产品都只是一个面向开发的工具) 在前期解决数据一致性,对账等常用的基本需求上,我们可以依赖于这些数据和测试的服务,展开更多的业务形态。 实时质量需要不断突破测试的边界 测试的边界在哪里? 过去有人告诉我,不能去修改业务应用的代码,只能让在盒子外面或者调用的方式来测试。还有人说,我们只开发工具,不能接触任何的业务。现在这些都在逐渐模糊,大家努力一起,让测试的很多活动,从简单的功能测试,往研发工具和业务质量等或前或后地迁移。 在过去的一两年,我们团队也已经慢慢承接了更多的职责,有些甚至于是直接服务于客服、运营和产品人员的。我认为,一支强的团队一定是不断走在突破原来工作边界的道路上。没有什么是一成不变的。 但每个职能团队都是有自己的核心价值的,而至于哪些应该由测试来做,哪些由开发做。我们的标准是:判断这件事情是更为了“让技术更有品质”还是“让技术创造新商业”?(“让技术更有品质”是我们团队的使命,“让技术拓展业务边界”是开发团队的目标) 以下虽然是几年前的例子,但也很好的体现了我们在边界的突破,以及如何用实时质量的思想来开装自己,创造提交 BUG 以外更多的价值。 案例:Offer 360提升客服端实时质量能力 商品链路复杂,线上问题排查难度大,之前开发每天平均投入2-3个小时处理线上问题,但实际上大部分的问题都是正常业务逻辑,并且可以让客满或者技术支持自助查询的。因此,我们通过提供实时查询错误日志以及 debug 信息的服务,把用户反馈问题的排查,开放给客服。帮助他们第一时间解决用户的问题。 实时质量未来规划 实时质量是一种思想,我觉得它未来是可以跨越在当前两种不同的发展分支上的。 测试这么多年来一直被弱化,我也看到集团很多优秀的测试 leader 转型开发、产品。如果我们还不多些思考,多些探索。如果做测试都还没有梦想,那跟咸鱼有什么区别? 图示:测试未来的发展 后记 上周在内部的论坛上看到一个开发专家的留言,还是挺有感触的。我们一直以来都在强调测试能力不断演进,强调开发能力,但测试的初心不能丢。我们在工具、测试能力上不断改进,但是从人和组织的角度上来看,在追求最高效的同时,我们是需要一定的组织设计来形成岗位间的相互监督。这也是在测试1.0阶段开始,测试被赋予的一种职责。 作者: 傲野 原文链接 ​ 本文为云栖社区原创内容,未经允许不得转载。
来源:OSCHINA
发布时间:2019-06-11 12:18:00
阿里云对中国用户来说是最好的云计算服务厂商了,而它提供的优惠也多种多样,不管对玩主机的新手还是老手,面对各种各样的活动都会陷入眼花缭乱的困境,因此,我专门写一篇文章来给大家抽丝剥茧,详细描绘一下对不同的用户怎么购买主机才是最优惠的。 优惠活动图 本人是拥有十年程序猿开发经验,大学本科四年,工作6年,软件工程出身,所以我用我们程序猿特有的软件来绘制一个优惠活动图。 大概花了半个多小时绘制玩这张活动图 /(ㄒoㄒ)/~~ 名词解释: 新用户:没注册过阿里云的用户或者注册且实名了但从未购买任何产品的用户 首购用户:表示已注册并实名认证后但从未购买该产品的用户 老用户:已实名并购买过该产品的用户 活动列表: 阿里云新用户2000元新手红包 阿里云Hi拼团活动 阿里云首购3折活动 优惠路线 新用户路线 领取2000元红包 —> 找老用户 开hi拼团活动 —> 2.4折优惠 领取2000元红包 —> 参加首购活动 —> 3折优惠 首购用户路线 找新用户 开hi拼团活动 —> 可以找到 —> 2.4折优惠 找新用户 开hi拼团活动 —> 没找到 —> 3折优惠 参加 首购活动 —> 3折优惠 老用户且非首购路线 找新用户 开hi拼团活动 —> 可以找到 —> 2.4折优惠 找新用户 开hi拼团活动 —> 没找到 —> 直接购买 —> 3折优惠 花了大概一个多小时认真写了这篇,也是想给自己刚上线的网站【 zhaozhuji.info 】做做推广,谢谢拜访! 其实做这个图也是得益于我的一个微信群"独立开发者"的,感谢小程序【魅力拍】作者徐玉丰,算也是给他做做推广,感谢他的启发!
来源:OSCHINA
发布时间:2019-06-08 19:29:00
或许很多人可能认为资源消耗并非安全问题,但实际上不合理的资源消耗会让黑客有可乘之机,来攻击K8s的组件。本文将介绍如何处理资源消耗或noisy neighbor问题,包括如何管理Pods中的资源以及管理项目和资源配额等。 本文是关于Kubernetes安全系列三篇文章中的最后一篇。在第一篇文章中,我们分享了如何确保企业的Kubernetes集群免受外部攻击;第二篇文章介绍了三种保护Kubernetes免受内部威胁的方法。在本文中,我们将介绍如何处理资源消耗或noisy neighbor问题。 对于那些设置了多租户Kubernetes集群的集群管理员而言,他们十分关注和担心的一个问题是,如何防止共同租户成为“noisy neighbor”,即一个垄断了CPU、内存、存储和其他资源的人。Noisy neighbor会对共享基础设施的其他用户资源的性能产生极坏的影响。 如此一来,跟踪Kubernetes容器和Pod的资源使用情况,对集群管理而言非常重要,因为它不仅可以保持容器编排系统处于最佳运行状态,降低运维成本,还可以加强Kubernetes的整体安全状况。 一些运维团队可能不认为资源消耗是一种重要的安全问题,至少没有保护Kubernetes免受内部和外部网络攻击重要。但这种观点是不正确的。因为厉害的黑客会利用功能不良的基础设施,来找到攻击Kubernetes组件的方法。 “安全不仅仅是‘不要闯进我的房子’,而是‘我怎么能让我的房子一直保持良好的运行状态’,”Rancher Labs的高级解决方案架构师Adrian Goins表示。 运维团队需要最大限度地利用Kubernetes Pods(一组具有共享存储和网络资源的一个或多个容器)所消耗的资源,以确保每个用户都能拥有最佳性能,并且能监控成本分配的使用情况。“使用等于成本,”Goins说,“因为Kubernetes资源都是运行在AWS、谷歌云、阿里云等等云提供商的底层计算基础设施上,一切资源消耗都以为着金钱成本。即使集群是在数据中心的裸机上运行,过多的使用也会花费硬件、电力和其他资源。” 默认情况下,配置容器时,对其可以使用的资源量没有任何限制。如果容器不能高效运行,部署容器的组织必将支付超额费用。值得庆幸的是,Kubernetes具有帮助运维团队管理和优化Kubernetes资源利用能力的功能。 管理Pods中的资源 当管理员定义Pod时,他们可以选择指定每个容器需要多少CPU和内存(RAM)。当容器指定了资源请求时,调度程序可以更好地决定将Pod放在哪个节点上。根据Kubernetes的文档,当容器指定了限制时,可以按指定的方式处理节点上的资源争用。 默认情况下,Kubernetes集群中的所有资源都是在默认的命名空间中创建的。命名空间是一种逻辑地将集群资源进行分组的方法,包括用于指定资源配额的选项。 管理员可以在命名空间上设置资源限制或配额,为在命名空间中运行的工作负载或应用程序分配一定量的CPU、RAM或存储——Kubernetes集群中的三个资源。“如果在命名空间中启动另一个资源会超出预设的配额,那么任何新资源都无法启动,”Goins指出。 “当你应用了资源配额时,意味着你强制在该命名空间中运行的所有内容为其自身设置资源限制。限制有两种类型:预留,和最大限制,”Goins解释说。例如,通过预留,管理员可以让Kubernetes集群为WordPress站点分配128 MB的RAM。对于部署的每个WordPress Pod,服务器本身将保证128 MB的RAM。因此,如果管理员将资源请求与1GB的资源配额相结合,则用户只能在超过其限制之前运行八个WordPress Pod。在那之后,他们将无法再使用RAM了。 资源限制的第二部分是最大限度。管理员可以预留128 MB的资源请求和最多256 MB的RAM。“如果Pod超过256 MB的RAM使用量,Kubernetes会杀死它并重新启动它,”Goins说。“如此以来,用户可以免受失控过程和noisy neighbor的影响。” 项目和资源配额 像Rancher这样的平台,旨在通过提供直观的界面和集中管理任务(如全局层的角色描述)来简化Kubernetes的管理。 正如前一篇关于内部威胁防护的文章所述,Rancher包含一个有助于减轻集群管理负担的“项目(Project)”资源,来超越命名空间。在Rancher中,Project允许管理员将多个命名空间作为单个实体进行管理。因此,Rancher可以将资源配额应用于Projects。 在标准Kubernetes部署中,资源配额只能应用于单独的命名空间。但是,管理员无法通过单次操作,同时将配额应用于命名空间。资源配额必须经过多次操作。 然而在Rancher中,管理员可以将资源配额应用于Project,然后将配额传播到每个命名空间。然后,Kubernetes会使用本机版本的资源配额,来强制执行管理员限制。如果管理员希望更改特定命名空间的配额,则可以覆盖以前的配额。 强化和优化Kubernetes 毋庸置疑,Kubernetes已成为容器编排的标准,这也促使大多数云和虚拟化供应商将其作为标准基础架构来提供。但是,对与Kubernetes环境相关的安全问题的普遍缺乏认识,可能会使各种组件暴露于来自网络集群内外的攻击中。 本系列文章的上两篇中提供了一些可行的步骤,来告诉大家如何通过使用Kubernetes功能和容器管理解决方案(如Rancher),来加强Kubernetes对外部和内部网络威胁的防范。企业应通过基于角色的访问控制(RBAC)和强身份验证从外部保护Kubernetes API访问。对于内部人员保护,由于Kubernetes集群是多用户,因此组织需要通过RBAC、逻辑隔离和NetworkPolicies来保护交叉通信。 为了防止其他租户垄断CPU、内存、存储和其他资源从而拖累整个集群的性能,Kubernetes提供资源限制和配额等功能,以帮助运维团队管理和优化Kubernetes资源利用功能。最后,除了可用的默认设置之外,业界还有一些非常有效的工具可以帮助用户完成Kubernetes集群的管理和保护。例如像Rancher这样的平台就是一种高度优化的容器管理解决方案,专为将多个集群部署到生产环境中的组织而构建,企业用户可以更轻松地管理和运行各地的Kubernetes。它可以保护Kubernetes集群免受外部黑客威胁、内部隐患甚至noisy neighbor。
来源:OSCHINA
发布时间:2019-07-03 10:50:00
元数据服务是 BeeGFS 中用来维护文件和目录关系及其属性配置的服务,其多线程epoll设计实现非常高效,主要流程如下: ConnAcceptor(PThread)类(一个线程)负责监听端口,并接受客户端连接,然后把;连接信息(包含接收的套接字)写入管道; StreamListenerV2(PThread)类(多个线程,可配置)从管道读取连接信息,使用epoll轮询接收数据,然后生成IncomingPreprocessedMsgWork(Work),写入MultiWorkQueue先进先出队列; Worker(PThread)类(多个线程,可配置)从MultiWorkQueue队列取出消息进行处理。 程序初始化 主函数 创建App对象,App对象是程序的主要载体: // fhgfs_meta\source\program\main.cpp #include "Program.h" int main(int argc, char** argv) { return Program::main(argc, argv); } // fhgfs_meta\source\program\Program.cpp #include #include "Program.h" #include App* Program::app = NULL; int Program::main(int argc, char** argv) { BuildTypeTk::checkDebugBuildTypes(); AbstractApp::runTimeInitsAndChecks(); // must be called before creating a new App app = new App(argc, argv); app->startInCurrentThread(); int appRes = app->getAppResult(); delete app; return appRes; } 创建ConnAcceptor 主程序中会初始化一个线程,监听服务端口,由ConnAcceptor类负责: // fhgfs_meta\source\app\App.cpp void App::initComponents(TargetConsistencyState initialConsistencyState) throw(ComponentInitException) { this->log->log(Log_DEBUG, "Initializing components..."); this->dgramListener = new DatagramListener( netFilter, localNicList, ackStore, cfg->getConnMetaPortUDP() ); if(cfg->getTuneListenerPrioShift() ) dgramListener->setPriorityShift(cfg->getTuneListenerPrioShift() ); streamListenersInit(); unsigned short listenPort = cfg->getConnMetaPortTCP(); this->connAcceptor = new ConnAcceptor(this, localNicList, listenPort); this->statsCollector = new StatsCollector(workQueue, STATSCOLLECTOR_COLLECT_INTERVAL_MS, STATSCOLLECTOR_HISTORY_LENGTH); this->buddyResyncer = new BuddyResyncer(); this->internodeSyncer = new InternodeSyncer(initialConsistencyState); this->timerQueue = new TimerQueue(1, 1); this->modificationEventFlusher = new ModificationEventFlusher(); workersInit(); commSlavesInit(); this->log->log(Log_DEBUG, "Components initialized."); } 创建StreamListener 根据配置创建多个StreamListener实例,每个实例对应线程,用于从ConnAcceptor接收新连接,已及从从连接读取数据,生成Work: // fhgfs_meta\source\app\App.cpp void App::streamListenersInit() throw(ComponentInitException) { this->numStreamListeners = cfg->getTuneNumStreamListeners(); for(unsigned i=0; i < numStreamListeners; i++) { StreamListenerV2* listener = new StreamListenerV2( std::string("StreamLis") + StringTk::uintToStr(i+1), this, workQueue); if(cfg->getTuneListenerPrioShift() ) listener->setPriorityShift(cfg->getTuneListenerPrioShift() ); if(cfg->getTuneUseAggressiveStreamPoll() ) listener->setUseAggressivePoll(); streamLisVec.push_back(listener); } } 创建WorkQueue 创建WorkQueue,用于保存StreamListener生成的Work: // fhgfs_meta\source\app\App.cpp /** * Init basic shared objects like work queues, node stores etc. */ void App::initDataObjects() throw(InvalidConfigException) { ... this->workQueue = new MultiWorkQueue(); this->commSlaveQueue = new MultiWorkQueue(); if(cfg->getTuneUsePerUserMsgQueues() ) workQueue->setIndirectWorkList(new UserWorkContainer() ); ... } 创建Worker 根据配置创建Worker线程,从WorkQueue读取Work并进行处理: // fhgfs_meta\source\app\App.cpp void App::workersInit() throw(ComponentInitException) { unsigned numWorkers = cfg->getTuneNumWorkers(); for(unsigned i=0; i < numWorkers; i++) { Worker* worker = new Worker( std::string("Worker") + StringTk::uintToStr(i+1), workQueue, QueueWorkType_INDIRECT); worker->setBufLens(cfg->getTuneWorkerBufSize(), cfg->getTuneWorkerBufSize() ); workerList.push_back(worker); } for(unsigned i=0; i < APP_WORKERS_DIRECT_NUM; i++) { Worker* worker = new Worker( std::string("DirectWorker") + StringTk::uintToStr(i+1), workQueue, QueueWorkType_DIRECT); worker->setBufLens(cfg->getTuneWorkerBufSize(), cfg->getTuneWorkerBufSize() ); workerList.push_back(worker); } } 连接监听 监听类ConnAcceptor ConnAcceptor类的定义: // fhgfs_common\source\common\components\streamlistenerv2\ConnAcceptor.h class ConnAcceptor : public PThread { public: ConnAcceptor(AbstractApp* app, NicAddressList& localNicList, unsigned short listenPort) throw(ComponentInitException); virtual ~ConnAcceptor(); private: AbstractApp* app; LogContext log; StandardSocket* tcpListenSock; StandardSocket* sdpListenSock; RDMASocket* rdmaListenSock; int epollFD; bool initSocks(unsigned short listenPort, NicListCapabilities* localNicCaps); virtual void run(); void listenLoop(); void onIncomingStandardConnection(StandardSocket* sock); void onIncomingRDMAConnection(RDMASocket* sock); void applySocketOptions(StandardSocket* sock); public: // getters & setters }; 连接监听循环 使用epool来轮询监听端口,并建立新连接: // fhgfs_common\source\common\components\streamlistenerv2\ConnAcceptor.cpp void ConnAcceptor::run() { try { registerSignalHandler(); listenLoop(); log.log(Log_DEBUG, "Component stopped."); } catch(std::exception& e) { PThread::getCurrentThreadApp()->handleComponentException(e); } } void ConnAcceptor::listenLoop() { const int epollTimeoutMS = 3000; struct epoll_event epollEvents[EPOLL_EVENTS_NUM]; // (just to have these values on the stack...) const int epollFD = this->epollFD; RDMASocket* rdmaListenSock = this->rdmaListenSock; StandardSocket* sdpListenSock = this->sdpListenSock; StandardSocket* tcpListenSock = this->tcpListenSock; // wait for incoming events and handle them... while(!getSelfTerminate() ) { //log.log(Log_DEBUG, std::string("Before poll(). pollArrayLen: ") + // StringTk::uintToStr(pollArrayLen) ); int epollRes = epoll_wait(epollFD, epollEvents, EPOLL_EVENTS_NUM, epollTimeoutMS); if(unlikely(epollRes < 0) ) { // error occurred if(errno == EINTR) // ignore interruption, because the debugger causes this continue; log.logErr(std::string("Unrecoverable epoll_wait error: ") + System::getErrString() ); break; } // handle incoming connection attempts for(size_t i=0; i < (size_t)epollRes; i++) { struct epoll_event* currentEvent = &epollEvents[i]; Pollable* currentPollable = (Pollable*)currentEvent->data.ptr; //log.log(Log_DEBUG, std::string("Incoming data on FD: ") + // StringTk::intToStr(pollArray[i].fd) ); // debug in if(currentPollable == rdmaListenSock) onIncomingRDMAConnection(rdmaListenSock); else if(currentPollable == tcpListenSock) onIncomingStandardConnection(tcpListenSock); else if(currentPollable == sdpListenSock) onIncomingStandardConnection(sdpListenSock); else { // unknown connection => should never happen log.log(Log_WARNING, "Should never happen: Ignoring event for unknown connection. " "FD: " + StringTk::uintToStr(currentPollable->getFD() ) ); } } } } 套接字监听处理(派发给流) 把建立的套接字发送给指定的StreamListener: // fhgfs_common\source\common\components\streamlistenerv2\ConnAcceptor.cpp /** * Accept the incoming connection and add new socket to StreamListenerV2 queue. * * Note: This is for standard sockets like TCP and SDP. */ void ConnAcceptor::onIncomingStandardConnection(StandardSocket* sock) { try { struct sockaddr_in peerAddr; socklen_t peerAddrLen = sizeof(peerAddr); StandardSocket* acceptedSock = (StandardSocket*)sock->accept( (struct sockaddr*)&peerAddr, &peerAddrLen); // (note: level Log_DEBUG to avoid spamming the log until we have log topics) log.log(Log_DEBUG, std::string("Accepted new connection from " + Socket::endpointAddrToString(&peerAddr.sin_addr, ntohs(peerAddr.sin_port) ) ) + std::string(" [SockFD: ") + StringTk::intToStr(acceptedSock->getFD() ) + std::string("]") ); applySocketOptions(acceptedSock); // hand the socket over to a stream listener StreamListenerV2* listener = app->getStreamListenerByFD(acceptedSock->getFD() ); StreamListenerV2::SockReturnPipeInfo returnInfo( StreamListenerV2::SockPipeReturn_NEWCONN, acceptedSock); listener->getSockReturnFD()->write(&returnInfo, sizeof(returnInfo) ); } catch(SocketException& se) { log.logErr(std::string("Trying to continue after connection accept error: ") + se.what() ); } } 流处理的选择 选择StreamListener时,是根据fd的数值取模运算得来: // fhgfs_meta\source\app\App.h class App : public AbstractApp { public: /** * Get one of the available stream listeners based on the socket file descriptor number. * This is to load-balance the sockets over all available stream listeners and ensure that * sockets are not bouncing between different stream listeners. * * Note that IB connections eat two fd numbers, so 2 and multiples of 2 might not be a good * value for number of stream listeners. */ virtual StreamListenerV2* getStreamListenerByFD(int fd) { return streamLisVec[fd % numStreamListeners]; } } 数据包流处理 流处理类StreamListenerV2 StreamListener的定义: // fhgfs_common\source\common\components\streamlistenerv2\StreamListenerV2.h class StreamListenerV2 : public PThread { public: /** * This is what we will send over the socket return pipe */ struct SockReturnPipeInfo { /** * Standard constructor for creating/sending a returnInfo. */ SockReturnPipeInfo(SockPipeReturnType returnType, Socket* sock) : returnType(returnType), sock(sock) {} /** * For receiving only (no initialization of members). */ SockReturnPipeInfo() {} SockPipeReturnType returnType; Socket* sock; }; } 流处理循环 StreamListener使用epoll同时处理新连接以及数据接收: // fhgfs_common\source\common\components\streamlistenerv2\StreamListenerV2.cpp void StreamListenerV2::run() { try { registerSignalHandler(); listenLoop(); log.log(Log_DEBUG, "Component stopped."); } catch(std::exception& e) { PThread::getCurrentThreadApp()->handleComponentException(e); } } void StreamListenerV2::listenLoop() { const int epollTimeoutMS = useAggressivePoll ? 0 : 3000; struct epoll_event epollEvents[EPOLL_EVENTS_NUM]; // (just to have these values on the stack...) const int epollFD = this->epollFD; FileDescriptor* sockReturnPipeReadEnd = this->sockReturnPipe->getReadFD(); bool runRDMAConnIdleCheck = false; // true just means we call the method (not enforce the check) // wait for incoming events and handle them... while(!getSelfTerminate() ) { //log.log(Log_DEBUG, std::string("Before poll(). pollArrayLen: ") + // StringTk::uintToStr(pollArrayLen) ); int epollRes = epoll_wait(epollFD, epollEvents, EPOLL_EVENTS_NUM, epollTimeoutMS); if(unlikely(epollRes < 0) ) { // error occurred if(errno == EINTR) // ignore interruption, because the debugger causes this continue; log.logErr(std::string("Unrecoverable epoll_wait error: ") + System::getErrString() ); break; } else if(unlikely(!epollRes || (rdmaCheckForceCounter++ > RDMA_CHECK_FORCE_POLLLOOPS) ) ) { // epollRes==0 is nothing to worry about, just idle // note: we can't run idle check here directly because the check might modify the // poll set, which will be accessed in the loop below runRDMAConnIdleCheck = true; } // handle incoming data & connection attempts for(size_t i=0; i < (size_t)epollRes; i++) { struct epoll_event* currentEvent = &epollEvents[i]; Pollable* currentPollable = (Pollable*)currentEvent->data.ptr; if(currentPollable == sockReturnPipeReadEnd) onSockReturn(); else onIncomingData( (Socket*)currentPollable); } if(unlikely(runRDMAConnIdleCheck) ) { // note: whether check actually happens depends on elapsed time since last check runRDMAConnIdleCheck = false; rdmaConnIdleCheck(); } } } 新连接处理 如果是新连接,则加入epoll的fd中: // fhgfs_common\source\common\components\streamlistenerv2\StreamListenerV2.cpp /** * Receive pointer to returned socket through the sockReturnPipe and re-add it to the pollList. */ void StreamListenerV2::onSockReturn() { SockReturnPipeInfo returnInfos[SOCKRETURN_SOCKS_NUM]; // try to get multiple returnInfos at once (if available) ssize_t readRes = sockReturnPipe->getReadFD()->read(&returnInfos, sizeof(SockReturnPipeInfo) ); // loop: walk over each info and handle the contained socket for(size_t i=0; ; i++) { SockReturnPipeInfo& currentReturnInfo = returnInfos[i]; // make sure we have a complete SockReturnPipeInfo if(unlikely(readRes < (ssize_t)sizeof(SockReturnPipeInfo) ) ) { // only got a partial SockReturnPipeInfo => recv the rest char* currentReturnInfoChar = (char*)¤tReturnInfo; sockReturnPipe->getReadFD()->readExact( ¤tReturnInfoChar[readRes], sizeof(SockReturnPipeInfo)-readRes); readRes = sizeof(SockReturnPipeInfo); } // handle returnInfo depending on contained returnType... Socket* currentSock = currentReturnInfo.sock; SockPipeReturnType returnType = currentReturnInfo.returnType; switch(returnType) { case SockPipeReturn_MSGDONE_NOIMMEDIATE: { // most likely case: worker is done with a msg and now returns the sock to the epoll set struct epoll_event epollEvent; epollEvent.events = EPOLLIN | EPOLLONESHOT | EPOLLET; epollEvent.data.ptr = currentSock; int epollRes = epoll_ctl(epollFD, EPOLL_CTL_MOD, currentSock->getFD(), &epollEvent); if(likely(!epollRes) ) { // sock was successfully re-armed in epoll set pollList.add(currentSock); break; // break out of switch } else if(errno != ENOENT) { // error log.logErr("Unable to re-arm sock in epoll set. " "FD: " + StringTk::uintToStr(currentSock->getFD() ) + "; " "SockTypeNum: " + StringTk::uintToStr(currentSock->getSockType() ) + "; " "SysErr: " + System::getErrString() ); log.log(Log_NOTICE, "Disconnecting: " + currentSock->getPeername() ); delete(currentSock); break; // break out of switch } /* for ENOENT, we fall through to NEWCONN, because this socket appearently wasn't used with this stream listener yet, so we need to add it (instead of modify it) */ } // might fall through here on ENOENT case SockPipeReturn_NEWCONN: { // new conn from ConnAcceptor (or wasn't used with this stream listener yet) // add new socket file descriptor to epoll set struct epoll_event epollEvent; epollEvent.events = EPOLLIN | EPOLLONESHOT | EPOLLET; epollEvent.data.ptr = currentSock; int epollRes = epoll_ctl(epollFD, EPOLL_CTL_ADD, currentSock->getFD(), &epollEvent); if(likely(!epollRes) ) { // socket was successfully added to epoll set pollList.add(currentSock); } else { // adding to epoll set failed => unrecoverable error log.logErr("Unable to add sock to epoll set. " "FD: " + StringTk::uintToStr(currentSock->getFD() ) + " " "SockTypeNum: " + StringTk::uintToStr(currentSock->getSockType() ) + " " "SysErr: " + System::getErrString() ); log.log(Log_NOTICE, "Disconnecting: " + currentSock->getPeername() ); delete(currentSock); } } break; case SockPipeReturn_MSGDONE_WITHIMMEDIATE: { // special case: worker detected that immediate data is available after msg processing // data immediately available => recv header and so on onIncomingData(currentSock); } break; default: { // should never happen: unknown/unhandled returnType log.logErr("Should never happen: " "Unknown socket return type: " + StringTk::uintToStr(returnType) ); } break; } // end of switch(returnType) readRes -= sizeof(SockReturnPipeInfo); if(!readRes) break; // all received returnInfos have been processed } // end of "for each received SockReturnPipeInfo" loop } 数据包处理(生成工作) 生成Work(IncomingPreprocessedMsgWork),并放进队列(MultiWorkQueue): // fhgfs_common\source\common\components\streamlistenerv2\StreamListenerV2.cpp /** * Receive msg header and add the socket to the work queue. */ void StreamListenerV2::onIncomingData(Socket* sock) { // check whether this is just a false alarm from a RDMASocket if( (sock->getSockType() == NICADDRTYPE_RDMA) && isFalseAlarm( (RDMASocket*)sock) ) { return; } try { const int recvTimeoutMS = 5000; char msgHeaderBuf[NETMSG_HEADER_LENGTH]; NetMessageHeader msgHeader; // receive & deserialize message header sock->recvExactT(msgHeaderBuf, NETMSG_HEADER_LENGTH, 0, recvTimeoutMS); NetMessage::deserializeHeader(msgHeaderBuf, NETMSG_HEADER_LENGTH, &msgHeader); /* (note on header verification: we leave header verification work to the worker threads to save CPU cycles in the stream listener and instead just take what we need to know here, no matter whether the header is valid or not.) */ // create work and add it to queue //log.log(Log_DEBUG, "Creating new work for to the queue"); IncomingPreprocessedMsgWork* work = new IncomingPreprocessedMsgWork(app, sock, &msgHeader); int sockFD = sock->getFD(); /* note: we store this here for delayed pollList removal, because worker thread might disconnect, so the sock gets deleted by the worker and thus "sock->" pointer becomes invalid */ sock->setHasActivity(); // mark sock as active (for idle disconnect check) // (note: userID intToStr (not uint) because default userID (~0) looks better this way) LOG_DEBUG("StreamListenerV2::onIncomingData", Log_DEBUG, "Incoming message: " + NetMsgStrMapping().defineToStr(msgHeader.msgType) + "; " "from: " + sock->getPeername() + "; " "userID: " + StringTk::intToStr(msgHeader.msgUserID) + (msgHeader.msgTargetID ? "; targetID: " + StringTk::uintToStr(msgHeader.msgTargetID) : "") ); if (sock->getIsDirect()) getWorkQueue(msgHeader.msgTargetID)->addDirectWork(work, msgHeader.msgUserID); else getWorkQueue(msgHeader.msgTargetID)->addIndirectWork(work, msgHeader.msgUserID); /* notes on sock handling: *) no need to remove sock from epoll set, because we use edge-triggered mode with oneshot flag (which disables further events after first one has been reported). *) a sock that is closed by a worker is not a problem, because it will automatically be removed from the epoll set by the kernel. *) we just need to re-arm the epoll entry upon sock return. */ pollList.removeByFD(sockFD); return; } catch(SocketTimeoutException& e) { log.log(Log_NOTICE, "Connection timed out: " + sock->getPeername() ); } catch(SocketDisconnectException& e) { // (note: level Log_DEBUG here to avoid spamming the log until we have log topics) log.log(Log_DEBUG, std::string(e.what() ) ); } catch(SocketException& e) { log.log(Log_NOTICE, "Connection error: " + sock->getPeername() + ": " + std::string(e.what() ) ); } // socket exception occurred => cleanup pollList.removeByFD(sock->getFD() ); IncomingPreprocessedMsgWork::invalidateConnection(sock); // also includes delete(sock) } 工作处理 工人类(Worker) // fhgfs_common\source\common\components\streamlistenerv2\ConnAcceptor.cpp #define WORKER_BUFIN_SIZE (1024*1024*4) #define WORKER_BUFOUT_SIZE WORKER_BUFIN_SIZE class Worker : public PThread { public: Worker(std::string workerID, MultiWorkQueue* workQueue, QueueWorkType workType) throw(ComponentInitException); virtual ~Worker() { SAFE_FREE(bufIn); SAFE_FREE(bufOut); } private: LogContext log; bool terminateWithFullQueue; // allow self-termination when queue not empty (see setter nodes) size_t bufInLen; char* bufIn; size_t bufOutLen; char* bufOut; MultiWorkQueue* workQueue; QueueWorkType workType; HighResolutionStats stats; virtual void run(); void workLoopAnyWork(); void workLoopDirectWork(); void initBuffers(); // inliners bool maySelfTerminateNow() { if(terminateWithFullQueue || (!workQueue->getDirectWorkListSize() && !workQueue->getIndirectWorkListSize() ) ) return true; return false; } public: // setters & getters /** * Note: Do not use this after the run method of this component has been called! */ void setBufLens(size_t bufInLen, size_t bufOutLen) { this->bufInLen = bufInLen; this->bufOutLen = bufOutLen; } /** * WARNING: This will only work if there is only a single worker attached to a queue. * Otherwise the queue would need a getWorkAndDontWait() method that is used during the * termination phase of the worker, because the queue might become empty before the worker * calls waitForWork() after the maySelfTerminateNow check. */ void disableTerminationWithFullQueue() { this->terminateWithFullQueue = false; } }; 工作类(Work) // fhgfs_common\source\common\components\worker\Work.h class Work { public: Work() { HighResolutionStatsTk::resetStats(&stats); } virtual ~Work() {} Work(const Work&) = delete; Work(Work&&) = delete; Work& operator=(const Work&) = delete; Work& operator=(Work&&) = delete; virtual void process(char* bufIn, unsigned bufInLen, char* bufOut, unsigned bufOutLen) = 0; protected: HighResolutionStats stats; public: HighResolutionStats* getHighResolutionStats() { return &stats; } #ifdef BEEGFS_DEBUG_PROFILING TimeFine* getAgeTime() { return &age; } private: TimeFine age; #endif }; // fhgfs_common\source\common\components\streamlistenerv2\IncomingPreprocessedMsgWork.h class IncomingPreprocessedMsgWork : public Work { public: /** * Note: Be aware that this class is only for stream connections that need to be returned * to a StreamListenerV2 after processing. * * @param msgHeader contents will be copied */ IncomingPreprocessedMsgWork(AbstractApp* app, Socket* sock, NetMessageHeader* msgHeader) { this->app = app; this->sock = sock; this->msgHeader = *msgHeader; } virtual void process(char* bufIn, unsigned bufInLen, char* bufOut, unsigned bufOutLen); static void releaseSocket(AbstractApp* app, Socket** sock, NetMessage* msg); static void invalidateConnection(Socket* sock); static bool checkRDMASocketImmediateData(AbstractApp* app, Socket* sock); private: AbstractApp* app; Socket* sock; NetMessageHeader msgHeader; }; 工作循环 从WorkQueens获取Work并进行处理: // fhgfs_common\source\common\components\worker\Worker.cpp void Worker::workLoop(QueueWorkType workType) { LOG(DEBUG, "Ready", as("TID", System::getTID()), workType); workQueue->incNumWorkers(); // add this worker to queue stats while(!getSelfTerminate() || !maySelfTerminateNow() ) { Work* work = waitForWorkByType(stats, personalWorkQueue, workType); #ifdef BEEGFS_DEBUG_PROFILING TimeFine workStartTime; #endif HighResolutionStatsTk::resetStats(&stats); // prepare stats // process the work packet work->process(bufIn, bufInLen, bufOut, bufOutLen); // update stats stats.incVals.workRequests = 1; HighResolutionStatsTk::addHighResIncStats(*work->getHighResolutionStats(), stats); #ifdef BEEGFS_DEBUG_PROFILING TimeFine workEndTime; const auto workElapsedMS = workEndTime.elapsedSinceMS(&workStartTime); const auto workLatencyMS = workEndTime.elapsedSinceMS(work->getAgeTime()); if (workElapsedMS >= 10) { if (workLatencyMS >= 10) LOG_TOP(WORKQUEUES, DEBUG, "Work processed.", as("Elapsed ms", workElapsedMS), as("Total latency (ms)", workLatencyMS)); else LOG_TOP(WORKQUEUES, DEBUG, "Work processed.", as("Elapsed ms", workElapsedMS), as("Total latency (us)", workEndTime.elapsedSinceMicro(work->getAgeTime()))); } else { if (workLatencyMS >= 10) { LOG_TOP(WORKQUEUES, DEBUG, "Work processed.", as("Elapsed us", workEndTime.elapsedSinceMicro(&workStartTime)), as("Total latency (ms)", workEndTime.elapsedSinceMS(work->getAgeTime()))); } else { LOG_TOP(WORKQUEUES, DEBUG, "Work processed.", as("Elapsed us", workEndTime.elapsedSinceMicro(&workStartTime)), as("Total latency (us)", workEndTime.elapsedSinceMicro(work->getAgeTime()))); } } #endif // cleanup delete(work); } } 工作处理(消息生成和处理) 处理Work时,使用Work基类的processIncoming虚函数进行处理: // fhgfs_common\source\common\components\streamlistenerv2\IncomingPreprocessedMsgWork.cpp void IncomingPreprocessedMsgWork::process(char* bufIn, unsigned bufInLen, char* bufOut, unsigned bufOutLen) { const char* logContextStr = "Work (process incoming msg)"; const int recvTimeoutMS = 5000; unsigned numReceived = NETMSG_HEADER_LENGTH; // (header actually received by stream listener) NetMessage* msg = NULL; try { // attach stats to sock (stream listener already received the msg header) stats.incVals.netRecvBytes += NETMSG_HEADER_LENGTH; sock->setStats(&stats); // make sure msg length fits into our receive buffer unsigned msgLength = msgHeader.msgLength; unsigned msgPayloadLength = msgLength - numReceived; if(unlikely(msgPayloadLength > bufInLen) ) { // message too big LogContext(logContextStr).log(Log_NOTICE, std::string("Received a message that is too large. Disconnecting: ") + sock->getPeername() ); sock->unsetStats(); invalidateConnection(sock); return; } // receive the message payload if(msgPayloadLength) sock->recvExactT(bufIn, msgPayloadLength, 0, recvTimeoutMS); // we got the complete message buffer => create msg object AbstractApp* app = PThread::getCurrentThreadApp(); ICommonConfig* cfg = app->getCommonConfig(); AbstractNetMessageFactory* netMessageFactory = app->getNetMessageFactory(); msg = netMessageFactory->createFromPreprocessedBuf(&msgHeader, bufIn, msgPayloadLength); if(unlikely(msg->getMsgType() == NETMSGTYPE_Invalid) ) { // message invalid LogContext(logContextStr).log(Log_NOTICE, std::string("Received an invalid message. Disconnecting: ") + sock->getPeername() ); sock->unsetStats(); invalidateConnection(sock); delete(msg); return; } // process the received msg bool processRes = false; if(likely(!cfg->getConnAuthHash() || sock->getIsAuthenticated() || (msg->getMsgType() == NETMSGTYPE_AuthenticateChannel) ) ) { // auth disabled or channel is auth'ed or this is an auth msg => process NetMessage::ResponseContext rctx(NULL, sock, bufOut, bufOutLen, &stats); processRes = msg->processIncoming(rctx); } else LogContext(logContextStr).log(Log_NOTICE, std::string("Rejecting message from unauthenticated peer: ") + sock->getPeername() ); // processing completed => cleanup bool needSockRelease = msg->getReleaseSockAfterProcessing(); delete(msg); msg = NULL; if(!needSockRelease) return; // sock release was already done within msg->processIncoming() method if(unlikely(!processRes) ) { // processIncoming encountered messaging error => invalidate connection LogContext(logContextStr).log(Log_NOTICE, std::string("Problem encountered during processing of a message. Disconnecting: ") + sock->getPeername() ); invalidateConnection(sock); return; } releaseSocket(app, &sock, NULL); return; } catch(SocketTimeoutException& e) { LogContext(logContextStr).log(Log_NOTICE, std::string("Connection timed out: ") + sock->getPeername() ); } catch(SocketDisconnectException& e) { // (note: level Log_DEBUG here to avoid spamming the log until we have log topics) LogContext(logContextStr).log(Log_DEBUG, std::string(e.what() ) ); } catch(SocketException& e) { LogContext(logContextStr).log(Log_NOTICE, std::string("Connection error: ") + sock->getPeername() + std::string(": ") + std::string(e.what() ) ); } // socket exception occurred => cleanup if(msg && msg->getReleaseSockAfterProcessing() ) { sock->unsetStats(); invalidateConnection(sock); } SAFE_DELETE(msg); } 消息工厂 消息工厂类(NetMessageFactory) StreamListener收到数据时使用消息工厂类生成各种类型的消息: // fhgfs_meta\source\net\message\NetMessageFactory.h class NetMessageFactory : public AbstractNetMessageFactory { public: NetMessageFactory() {} protected: virtual NetMessage* createFromMsgType(unsigned short msgType); } ; 消息工厂初始化 // fhgfs_meta\source\app\App.cpp /** * Init basic networking data structures. * * Note: no RDMA is detected here, because this needs to be done later */ void App::initBasicNetwork() { // check if management host is defined if(!cfg->getSysMgmtdHost().length() ) throw InvalidConfigException("Management host undefined"); // prepare filter for outgoing packets/connections this->netFilter = new NetFilter(cfg->getConnNetFilterFile() ); this->tcpOnlyFilter = new NetFilter(cfg->getConnTcpOnlyFilterFile() ); // prepare filter for interfaces StringList allowedInterfaces; std::string interfacesList = cfg->getConnInterfacesList(); if(!interfacesList.empty() ) { log->log(Log_DEBUG, "Allowed interfaces: " + interfacesList); StringTk::explodeEx(interfacesList, ',', true, &allowedInterfaces); } // discover local NICs and filter them NetworkInterfaceCard::findAllInterfaces(allowedInterfaces, cfg->getConnUseSDP(), localNicList); if(localNicList.empty() ) throw InvalidConfigException("Couldn't find any usable NIC"); localNicList.sort(&NetworkInterfaceCard::nicAddrPreferenceComp); // prepare factory for incoming messages this->netMessageFactory = new NetMessageFactory(); } 生成消息 消息实例的生成均根据msgType来确定: // fhgfs_meta\source\net\message\NetMessageFactory.cpp /** * @return NetMessage that must be deleted by the caller * (msg->msgType is NETMSGTYPE_Invalid on error) */ NetMessage* NetMessageFactory::createFromMsgType(unsigned short msgType) { NetMessage* msg; switch(msgType) { // The following lines are grouped by "type of the message" and ordered alphabetically inside // the groups. There should always be one message per line to keep a clear layout (although // this might lead to lines that are longer than usual) // control messages case NETMSGTYPE_Ack: { msg = new AckMsgEx(); } break; case NETMSGTYPE_AuthenticateChannel: { msg = new AuthenticateChannelMsgEx(); } break; case NETMSGTYPE_GenericResponse: { msg = new GenericResponseMsg(); } break; case NETMSGTYPE_SetChannelDirect: { msg = new SetChannelDirectMsgEx(); } break; case NETMSGTYPE_PeerInfo: { msg = new PeerInfoMsgEx(); } break; // nodes messages case NETMSGTYPE_ChangeTargetConsistencyStatesResp: { msg = new ChangeTargetConsistencyStatesRespMsg(); } break; case NETMSGTYPE_GenericDebug: { msg = new GenericDebugMsgEx(); } break; case NETMSGTYPE_GetClientStats: { msg = new GetClientStatsMsgEx(); } break; case NETMSGTYPE_GetMirrorBuddyGroupsResp: { msg = new GetMirrorBuddyGroupsRespMsg(); } break; case NETMSGTYPE_GetNodeCapacityPools: { msg = new GetNodeCapacityPoolsMsgEx(); } break; case NETMSGTYPE_GetNodeCapacityPoolsResp: { msg = new GetNodeCapacityPoolsRespMsg(); } break; case NETMSGTYPE_GetNodes: { msg = new GetNodesMsgEx(); } break; case NETMSGTYPE_GetNodesResp: { msg = new GetNodesRespMsg(); } break; case NETMSGTYPE_GetStatesAndBuddyGroupsResp: { msg = new GetStatesAndBuddyGroupsRespMsg(); } break; case NETMSGTYPE_GetTargetMappings: { msg = new GetTargetMappingsMsgEx(); } break; case NETMSGTYPE_GetTargetMappingsResp: { msg = new GetTargetMappingsRespMsg(); } break; case NETMSGTYPE_GetTargetStatesResp: { msg = new GetTargetStatesRespMsg(); } break; case NETMSGTYPE_HeartbeatRequest: { msg = new HeartbeatRequestMsgEx(); } break; case NETMSGTYPE_Heartbeat: { msg = new HeartbeatMsgEx(); } break; case NETMSGTYPE_MapTargets: { msg = new MapTargetsMsgEx(); } break; case NETMSGTYPE_PublishCapacities: { msg = new PublishCapacitiesMsgEx(); } break; case NETMSGTYPE_RegisterNodeResp: { msg = new RegisterNodeRespMsg(); } break; case NETMSGTYPE_RemoveNode: { msg = new RemoveNodeMsgEx(); } break; case NETMSGTYPE_RemoveNodeResp: { msg = new RemoveNodeRespMsg(); } break; case NETMSGTYPE_RefreshCapacityPools: { msg = new RefreshCapacityPoolsMsgEx(); } break; case NETMSGTYPE_RefreshTargetStates: { msg = new RefreshTargetStatesMsgEx(); } break; case NETMSGTYPE_SetMirrorBuddyGroup: { msg = new SetMirrorBuddyGroupMsgEx(); } break; case NETMSGTYPE_SetRootNodeIDResp: { msg = new SetRootNodeIDRespMsg(); } break; case NETMSGTYPE_SetTargetConsistencyStates: { msg = new SetTargetConsistencyStatesMsgEx(); } break; case NETMSGTYPE_SetTargetConsistencyStatesResp: { msg = new SetTargetConsistencyStatesRespMsg(); } break; // storage messages case NETMSGTYPE_FindEntryname: { msg = new FindEntrynameMsgEx(); } break; case NETMSGTYPE_FindLinkOwner: { msg = new FindLinkOwnerMsgEx(); } break; case NETMSGTYPE_FindOwner: { msg = new FindOwnerMsgEx(); } break; case NETMSGTYPE_FindOwnerResp: { msg = new FindOwnerRespMsg(); } break; case NETMSGTYPE_GetChunkFileAttribsResp: { msg = new GetChunkFileAttribsRespMsg(); } break; case NETMSGTYPE_GetStorageTargetInfo: { msg = new GetStorageTargetInfoMsgEx(); } break; case NETMSGTYPE_GetEntryInfo: { msg = new GetEntryInfoMsgEx(); } break; case NETMSGTYPE_GetEntryInfoResp: { msg = new GetEntryInfoRespMsg(); } break; case NETMSGTYPE_GetHighResStats: { msg = new GetHighResStatsMsgEx(); } break; case NETMSGTYPE_GetMetaResyncStats: { msg = new GetMetaResyncStatsMsgEx(); } break; case NETMSGTYPE_RequestExceededQuotaResp: {msg = new RequestExceededQuotaRespMsg(); } break; case NETMSGTYPE_SetExceededQuota: {msg = new SetExceededQuotaMsgEx(); } break; case NETMSGTYPE_StorageResyncStarted: { msg = new StorageResyncStartedMsgEx(); } break; case NETMSGTYPE_StorageResyncStartedResp: { msg = new StorageResyncStartedRespMsg(); } break; case NETMSGTYPE_GetXAttr: { msg = new GetXAttrMsgEx(); } break; case NETMSGTYPE_Hardlink: { msg = new HardlinkMsgEx(); } break; case NETMSGTYPE_HardlinkResp: { msg = new HardlinkRespMsg(); } break; case NETMSGTYPE_ListDirFromOffset: { msg = new ListDirFromOffsetMsgEx(); } break; case NETMSGTYPE_ListDirFromOffsetResp: { msg = new ListDirFromOffsetRespMsg(); } break; case NETMSGTYPE_ListXAttr: { msg = new ListXAttrMsgEx(); } break; case NETMSGTYPE_LookupIntent: { msg = new LookupIntentMsgEx(); } break; case NETMSGTYPE_LookupIntentResp: { msg = new LookupIntentRespMsg(); } break; case NETMSGTYPE_MkDir: { msg = new MkDirMsgEx(); } break; case NETMSGTYPE_MkDirResp: { msg = new MkDirRespMsg(); } break; case NETMSGTYPE_MkFile: { msg = new MkFileMsgEx(); } break; case NETMSGTYPE_MkFileResp: { msg = new MkFileRespMsg(); } break; case NETMSGTYPE_MkFileWithPattern: { msg = new MkFileWithPatternMsgEx(); } break; case NETMSGTYPE_MkFileWithPatternResp: { msg = new MkFileWithPatternRespMsg(); } break; case NETMSGTYPE_MkLocalDir: { msg = new MkLocalDirMsgEx(); } break; case NETMSGTYPE_MkLocalDirResp: { msg = new MkLocalDirRespMsg(); } break; case NETMSGTYPE_MkLocalFileResp: { msg = new MkLocalFileRespMsg(); } break; case NETMSGTYPE_MovingDirInsert: { msg = new MovingDirInsertMsgEx(); } break; case NETMSGTYPE_MovingDirInsertResp: { msg = new MovingDirInsertRespMsg(); } break; case NETMSGTYPE_MovingFileInsert: { msg = new MovingFileInsertMsgEx(); } break; case NETMSGTYPE_MovingFileInsertResp: { msg = new MovingFileInsertRespMsg(); } break; case NETMSGTYPE_RefreshEntryInfo: { msg = new RefreshEntryInfoMsgEx(); } break; case NETMSGTYPE_RefreshEntryInfoResp: { msg = new RefreshEntryInfoRespMsg(); } break; case NETMSGTYPE_ResyncRawInodes: { msg = new ResyncRawInodesMsgEx(); } break; case NETMSGTYPE_ResyncRawInodesResp: { msg = new ResyncRawInodesRespMsg(); } break; case NETMSGTYPE_ResyncSessionStore: { msg = new ResyncSessionStoreMsgEx(); } break; case NETMSGTYPE_ResyncSessionStoreResp: { msg = new ResyncSessionStoreRespMsg(); } break; case NETMSGTYPE_RemoveXAttr: { msg = new RemoveXAttrMsgEx(); } break; case NETMSGTYPE_RemoveXAttrResp: { msg = new RemoveXAttrRespMsg(); } break; case NETMSGTYPE_Rename: { msg = new RenameV2MsgEx(); } break; case NETMSGTYPE_RenameResp: { msg = new RenameRespMsg(); } break; case NETMSGTYPE_RmChunkPathsResp: { msg = new RmChunkPathsRespMsg(); } break; case NETMSGTYPE_RmDirEntry: { msg = new RmDirEntryMsgEx(); } break; case NETMSGTYPE_RmDir: { msg = new RmDirMsgEx(); } break; case NETMSGTYPE_RmDirResp: { msg = new RmDirRespMsg(); } break; case NETMSGTYPE_RmLocalDir: { msg = new RmLocalDirMsgEx(); } break; case NETMSGTYPE_RmLocalDirResp: { msg = new RmLocalDirRespMsg(); } break; case NETMSGTYPE_SetAttr: { msg = new SetAttrMsgEx(); } break; case NETMSGTYPE_SetAttrResp: { msg = new SetAttrRespMsg(); } break; case NETMSGTYPE_SetDirPattern: { msg = new SetDirPatternMsgEx(); } break; case NETMSGTYPE_SetDirPatternResp: { msg = new SetDirPatternRespMsg(); } break; case NETMSGTYPE_SetLocalAttrResp: { msg = new SetLocalAttrRespMsg(); } break; case NETMSGTYPE_SetMetadataMirroring: { msg = new SetMetadataMirroringMsgEx(); } break; case NETMSGTYPE_SetStorageTargetInfoResp: { msg = new SetStorageTargetInfoRespMsg(); } break; case NETMSGTYPE_SetXAttr: { msg = new SetXAttrMsgEx(); } break; case NETMSGTYPE_SetXAttrResp: { msg = new SetXAttrRespMsg(); } break; case NETMSGTYPE_Stat: { msg = new StatMsgEx(); } break; case NETMSGTYPE_StatResp: { msg = new StatRespMsg(); } break; case NETMSGTYPE_StatStoragePath: { msg = new StatStoragePathMsgEx(); } break; case NETMSGTYPE_StatStoragePathResp: { msg = new StatStoragePathRespMsg(); } break; case NETMSGTYPE_TruncFile: { msg = new TruncFileMsgEx(); } break; case NETMSGTYPE_TruncFileResp: { msg = new TruncFileRespMsg(); } break; case NETMSGTYPE_TruncLocalFileResp: { msg = new TruncLocalFileRespMsg(); } break; case NETMSGTYPE_UnlinkFile: { msg = new UnlinkFileMsgEx(); } break; case NETMSGTYPE_UnlinkFileResp: { msg = new UnlinkFileRespMsg(); } break; case NETMSGTYPE_UnlinkLocalFileResp: { msg = new UnlinkLocalFileRespMsg(); } break; case NETMSGTYPE_UpdateBacklinkResp: { msg = new UpdateBacklinkRespMsg(); } break; case NETMSGTYPE_UpdateDirParent: { msg = new UpdateDirParentMsgEx(); } break; case NETMSGTYPE_UpdateDirParentResp: { msg = new UpdateDirParentRespMsg(); } break; // session messages case NETMSGTYPE_BumpFileVersion: { msg = new BumpFileVersionMsgEx(); } break; case NETMSGTYPE_BumpFileVersionResp: { msg = new BumpFileVersionRespMsg(); } break; case NETMSGTYPE_OpenFile: { msg = new OpenFileMsgEx(); } break; case NETMSGTYPE_OpenFileResp: { msg = new OpenFileRespMsg(); } break; case NETMSGTYPE_CloseFile: { msg = new CloseFileMsgEx(); } break; case NETMSGTYPE_CloseFileResp: { msg = new CloseFileRespMsg(); } break; case NETMSGTYPE_CloseChunkFileResp: { msg = new CloseChunkFileRespMsg(); } break; case NETMSGTYPE_WriteLocalFileResp: { msg = new WriteLocalFileRespMsg(); } break; case NETMSGTYPE_FSyncLocalFileResp: { msg = new FSyncLocalFileRespMsg(); } break; case NETMSGTYPE_FLockAppend: { msg = new FLockAppendMsgEx(); } break; case NETMSGTYPE_FLockEntry: { msg = new FLockEntryMsgEx(); } break; case NETMSGTYPE_FLockEntryResp: { msg = new FLockEntryRespMsg(); } break; case NETMSGTYPE_FLockRange: { msg = new FLockRangeMsgEx(); } break; case NETMSGTYPE_FLockRangeResp: { msg = new FLockRangeRespMsg(); } break; case NETMSGTYPE_GetFileVersion: { msg = new GetFileVersionMsgEx(); } break; case NETMSGTYPE_AckNotify: { msg = new AckNotifiyMsgEx(); } break; case NETMSGTYPE_AckNotifyResp: { msg = new AckNotifiyRespMsg(); } break; //admon messages case NETMSGTYPE_RequestMetaData: { msg = new RequestMetaDataMsgEx(); } break; case NETMSGTYPE_GetNodeInfo: { msg = new GetNodeInfoMsgEx(); } break; // fsck messages case NETMSGTYPE_RetrieveDirEntries: { msg = new RetrieveDirEntriesMsgEx(); } break; case NETMSGTYPE_RetrieveInodes: { msg = new RetrieveInodesMsgEx(); } break; case NETMSGTYPE_RetrieveFsIDs: { msg = new RetrieveFsIDsMsgEx(); } break; case NETMSGTYPE_DeleteDirEntries: { msg = new DeleteDirEntriesMsgEx(); } break; case NETMSGTYPE_CreateDefDirInodes: { msg = new CreateDefDirInodesMsgEx(); } break; case NETMSGTYPE_FixInodeOwners: { msg = new FixInodeOwnersMsgEx(); } break; case NETMSGTYPE_FixInodeOwnersInDentry: { msg = new FixInodeOwnersInDentryMsgEx(); } break; case NETMSGTYPE_LinkToLostAndFound: { msg = new LinkToLostAndFoundMsgEx(); } break; case NETMSGTYPE_CreateEmptyContDirs: { msg = new CreateEmptyContDirsMsgEx(); } break; case NETMSGTYPE_UpdateFileAttribs: { msg = new UpdateFileAttribsMsgEx(); } break; case NETMSGTYPE_UpdateDirAttribs: { msg = new UpdateDirAttribsMsgEx(); } break; case NETMSGTYPE_RemoveInodes: { msg = new RemoveInodesMsgEx(); } break; case NETMSGTYPE_RecreateFsIDs: { msg = new RecreateFsIDsMsgEx(); } break; case NETMSGTYPE_RecreateDentries: { msg = new RecreateDentriesMsgEx(); } break; case NETMSGTYPE_FsckSetEventLogging: { msg = new FsckSetEventLoggingMsgEx(); } break; case NETMSGTYPE_AdjustChunkPermissions: { msg = new AdjustChunkPermissionsMsgEx(); } break; default: { msg = new SimpleMsg(NETMSGTYPE_Invalid); } break; } return msg; }
来源:OSCHINA
发布时间:2019-07-02 23:23:00
近日,Rancher Labs宣布在Rancher 2.3 Preview2版本上支持Istio,简化了Istio的安装和配置,让部署和管理Istio的旅程变得简单而快速。 近日,业界领先的容器管理软件提供商Rancher Labs(以下简称Rancher)宣布在Rancher 2.3 Preview 2版本上支持Istio,让部署和管理Istio的旅程变得简单而快速。 为什么选择Istio? Istio,以及整个Service Mesh技术,是近一两年来Kubernetes生态系统中最亮眼的明星之一。Istio增加了容错、金丝雀部署、A/B测试、监控、跟踪和可观察性、身份认证和授权,开发人员无需再测试或编写特定代码,即可实现上述功能。如此一来,开发人员可以只专注于他们的业务逻辑,将剩下的工作交给Kubernetes和Istio。 上面这些说法其实并不新鲜。早在大约10年前,PaaS供应商们就提出了类似的说法,甚至在一定程度上兑现了这一要求。但问题在于,他们的产品需要特定的语言和框架,并且在大部分情况下只能用于非常简单的应用程序。用户的工作负载也会和供应商独特的方案关联在一起。这就意味着如果您希望应用程序使用PaaS服务,您可能会被锁定相当长的一段时间。 但如今,对于容器和Kubernetes而言,这些限制、这些被锁定的风险都不复存在。只要您将您的应用程序容器化,Kubernetes就可以为您运行它。 Istio在Rancher 2.3 Preview 2中如何工作 大量Rancher用户喜欢Rancher平台的原因,就是Rancher让管理和操作Kubernetes及相关的工具和技术变得极其简单,且用户们不必担心会被特定的云供应商锁定。而如今对于Istio,我们采取了同样的方法,致力于带给用户同样的体验。 在Rancher 2.3 Preview中,我们为用户提供了一个简单而友好的用户界面,在UI中使用工具菜单,即可启动Istio。系统提供了合理的默认配置,用户也可以根据需要进行修改: 为了监控流量,Istio需要注入Envoy sidecar。在Rancher 2.3 Preview当中,用户可以为每个空间名称注入自动sidecar。一旦您勾选了这个选项,Rancher会将sidecar容器注入到每个工作负载当中: Rancher简化了Istio的安装和配置,内置了一个支持Kiali的仪表盘,用于流量和遥测的可视化,然后用Jaeger进行追踪,甚至还有自己的Prometheus和Grafana(与用于高级监控的实例不同)。 在启用自动sidecar注入的命名空间中部署工作负载后,您可以跳转到Istio菜单目录,观察微服务应用程序的流量: 点击Kiali、Jaeger、Prometheus或者Grafana,您将进入每个工具相应的用户界面,您可以在其中找到详细信息和选项: 正如前面所提到的,Istio的强大之处在于它能为您的服务带来诸如容错、断路、金丝雀部署等功能。要启用这些功能,您需要开发和应用适当的YAML文件。目前Windows工作负载还不支持Istio,因此不应在Windows集群中启用它。 结 语 Istio是当前Rancher及Kubernetes社区中最受关注的功能之一。但是,如何最达到Istio部署和管理的最佳实践,前路仍然漫长。在Rancher 2.3 Preview 2中,我们的目标是沿袭Rancher一如既往的理念,让部署和管理Istio的旅程变得简单而快速。 2019年6月20日,在Rancher于北京举办的千人容器技术盛典“2019企业容器创新大会”上,Rancher大中华区研发经理张浩在演讲中分享了Rancher 2.3 Preview的一系列新功能,包括正式支持Windows Kubernetes、镜像仓库、镜像扫描、服务网格、Google登陆、集群模版、集群安全扫描和集群自动扩缩容等等,并且demo了如何在Rancher中使用Istio进行金丝雀发布。您可在Rancher微信公众号(RancherLabs)后台回复“ECIC”获取大会完整PPT下载喔~ 有关发行说明和安装步骤,请访问GitHub: https://github.com/rancher/rancher/releases/tag/v2.3.0-alpha5
来源:OSCHINA
发布时间:2019-07-02 10:01:00
Dev Ops源于Development和 Op eration s 的组合 常见的定义 DevOps是一种重视“软件开发人员(Dev)”和“IT运维技术人员(Ops)”之间沟通合作的文化、运动或惯例。透过自动化“软件交付”和“架构变更”的流程,来使得构建、测试、发布软件能够更加地快捷、频繁和可靠。 下面这个戴明环也是常见的表达形式: 点击此处添加图片说明文字 蓝鲸在深度实践DevOps后,结合对DevOps理解和经验总结,重新定义了DevOps。即下图这6个英文单词的首字母组成: Do 、 Efficiency 、 Value 、 Open 、 Progress 、 Security 。 点击此处添加图片说明文字 结合这六个词、结合蓝鲸产品团队在今年6月蓝鲸DevOps活动上的分享以及个人理解,我们将从蓝鲸的视角展开来谈谈DevOps: Do实践:以实践为基础推行DevOps DevOps文化、理论体系的宣导者众多,各种大会也会去介绍各种“道、法、术”;大一些的企业基本都会有设立教练的角色,指导各个研发团队开展DevOps转型。但一些企业用户在听完各种“道、法、术”之后,要么是讲的听不懂,要么是懂也不会做、做也做不好。也有企业先找咨询公司做咨询,但咨询完后却不知道怎么落地。 蓝鲸DevOps认为DevOps的第一要素,就是实践,即所谓的“事上练”。没有实践过DevOps的经历就没有感悟,谈论再多的文化、理论,还不如贴近业务研发痛点,动手行动,用实践来验证想法和理论,点滴积累,绘成逐渐强大的DevOps体系。 Efficiency效能:效能是DevOps追求的目标 在我们开展实践之后,需要有目标。DevOps 根本的目标就是提升研发效能。 首先,效能体现在可以让大家可以“Focus On Your Job”。开发人员的职责是写代码和合并代码,合并代码完就去抽烟,其他的交给平台自动化执行;而不是去推动打包、申请资源、部署、测试、生产上线。 其次,效能体现在可以让大家在同一套平台中进行工作和协同,而不是在不同的工具中做不同的事情。一个企业IT部门有18套研发、测试、运维工具,这代表先进还是落后呢?很显然,这是一种落后的表现,因为这几乎将无法实现跨系统自动调度。蓝鲸DevOps平台可以将DevOps工具链进行整合,让不同的角色专注于其本职工作,达到提升效能的目标。 Value价值:DevOps必须输出价值 DevOps要为用户不断的输出价值,就要为DevOps体系中融入更多的提供价值的功能。例如: 在DevOps平台中加入质量红线,可以提供给用户来建立各种质量门禁,如:代码准入门禁、迭代验收门禁、发布控制门禁; 在DevOps平台中提供编译加速,帮助用户提升编译的性能; 在DevOps平台中提供构建资源池,在构建的时候可以自动调度构建资源,完成构建之后可以自动释放资源等等; 在DevOps平台中优化流水线的体验和原子,用户可以轻松组装出来各种复杂的业务场景…… 价值也应该是可以复制的,企业通常有多个团队同时开展多个项目,我们对某个项目团队进行了大量DevOps方面的改进,并邀请工信部对项目进行了DevOps能力成熟度评级,我们团队达到了3级。但是,其他的项目或团队呢?他们能否达到3级标准?我们在DevOps方面做出的努力,是否可以平行复制到其他团队? 蓝鲸DevOps有一个理念是—— 价值应被平行复制到各个项目团队 。每一个价值点的输出,都可以让用户真正的感受到DevOps所能带来的改变,这样才能把用户凝聚在平台上,而不是总是考虑哪里用得不顺,自己建立一套平台。蓝鲸DevOps带来的体系完善、效能提升,不是针对某个团队,而是可以平行复制到所有的研发团队,这就是最大的价值。 Open开放:以开放的心态面对各种场景 不同的企业甚至同一家企业的不同团队,其DevOps落地的进程和对DevOps的要求都有差异的,我们必须用开放的心态接受这种差异。 例如:蓝鲸DevOps平台里面有敏捷协同模块,可以管理项目的需求、任务、缺陷、迭代计划等等,但是许多传统行业,基于企业的研发管控制度等原因,已经建立了适合自己的需求管理平台、研发任务管理平台等工具平台,我们的解决方案是不断给用户洗脑让用户放弃现有的协同和管理模式,还是以开放的心态来面对客户现有的管理体系呢? 蓝鲸的选择是以开放的体系面对不同团队的需求,提供尽可能灵活的架构和工具,通过工具开放的方式来兼容不同团队的模式。蓝鲸本身也是面向CI-CD-CO的研运一体化平台。 Progress演进:持续交付核心在于不断演进 DevOps的一个重要理念就在于持续改进。我们可以通过各个子系统的数据进行整体的度量,来发现哪个项目、哪些环节经常出现停滞、失败率比较高、耗时比较长,并且进行针对性的改进。 例如:如果研发效能瓶颈在测试环节,就需要深究导致测试耗时长的问题。如果是因为没有引入自动化的测试、手工测试耗时较长,就可以逐步补充自动化测试用例;如果研发效能瓶颈需要人工响应才能推进,就可以引入自动化的流水线和优化研发流程,减少人工参与和不必要的审核节点。 只有通过不断的改进,企业才能将原来的每月迭代和发布,缩短为每周迭代和发布,甚至逐步改进为每天迭代和发布,最终达到Google、FaceBook等企业达到的1天若干次发布的效果。 各个团队可以跟自己比,每一阶段都相比前一阶段有进步,就是团队的自我发展。而蓝鲸DevOps平台也是不断演进的成果。 Security安全:安全是基础 一个企业级的DevOps平台,安全是非常重要的。研发人员电脑、代码库、构建机、测试环境、制品库都可能导致代码及软件包的泄露,这也导致游戏行业大量私服的出现。而软件上线之后还要考虑漏洞被利用、跨站攻击、数据窃取等等问题。 不论DevOps平台本身,还是从平台流出的制品,一切要以安全为依归。DevOps平台本身应该提供监、管、控手段,可以进行细粒度的权限控制,避免非法访问和非法窃取数据、代码、软件包。DevOps平台也应该提供代码扫描、安全扫描、质量红线等安全工具,可以独立运行或者结合到流水线里面自动调用,保证交付的软件的可靠性,给平台使用者以及产出软件的用户一个安全保障。 作者:方勇
来源:OSCHINA
发布时间:2019-07-23 18:15:00
本系列将利用阿里云容器服务,帮助您上手 Kubeflow Pipelines. 介绍 机器学习的工程复杂度,除了来自于常见的软件开发问题外,还和机器学习数据驱动的特点相关。而这就带来了其工作流程链路更长,数据版本失控,实验难以跟踪、结果难以重现,模型迭代成本巨大等一系列问题。为了解决这些机器学习固有的问题,很多企业构建了内部机器学习平台来管理机器学习生命周期,其中最有名的是 Google 的 Tensorflow Extended, Facebook 的 FBLearner Flow, Uber 的 Michelangelo,遗憾的是这些平台都需要绑定在公司内部的基础设施之上,无法彻底开源。而这些机器学习平台的骨架就是机器学习工作流系统,它可以让数据科学家灵活定义自己的机器学习流水线,重用已有的数据处理和模型训练能力,进而更好的管理机器学习生命周期。 谈到机器学习工作流平台,Google 的工程经验非常丰富,它的 TensorFlow Extended 机器学习平台支撑了 Google 的搜索,翻译,视频等核心业务;更重要的是其对机器学习领域工程效率问题的理解深刻,Google 的 Kubeflow 团队于 2018 年底开源了 Kubeflow Pipelines(KFP),  KFP 的设计与 Google 内部机器学习平台  TensorFlow Extended  一脉相承,唯一的区别是 KFP 运行在 Kubernetes 的平台上,TFX 是运行在 Borg 之上的。 什么是 Kubeflow Pipelines Kubeflow Pipelines 平台包括: 能够运行和追踪实验的管理控制台 能够执行多个机器学习步骤的工作流引擎 (Argo) 用来自定义工作流的 SDK,目前只支持 Python 而 Kubeflow Pipelines 的目标在于: 端到端的任务编排 : 支持编排和组织复杂的机器学习工作流,该工作流可以被直接触发,定时触发,也可以由事件触发,甚至可以实现由数据的变化触发; 简单的实验管理 : 帮助数据科学家尝试众多的想法和框架,以及管理各种试验。并实现从实验到生产的轻松过渡; 通过组件化方便重用 : 通过重用 Pipelines 和组件快速创建端到端解决方案,无需每次从 0 开始的重新构建。 在阿里云上运行 Kubeflow Pipelines 看到 Kubeflow Piplines 的能力,大家是不是都摩拳擦掌,想一睹为快?但是目前国内想使用 Kubeflow Pipeline 有两个挑战: Pipelines 需要通过 Kubeflow 部署;而 Kubeflow 默认组件过多,同时通过 Ksonnet 部署 Kubeflow 也是很复杂的事情; Pipelines 本身和谷歌云平台有深度耦合,无法运行在其他云平台上或者裸金属服务器的环境。 为了方便国内的用户安装 Kubeflow Pipelines,阿里云容器服务团队提供了基于 Kustomize 的 Kubeflow Pipelines 部署方案。和普通的 Kubeflow 基础服务不同,Kubeflow Pipelines 需要依赖于 mysql 和 minio 这些有状态服务,也就需要考虑如何持久化和备份数据。在本例子中,我们借助阿里云 SSD 云盘作为数据持久化的方案,分别自动的为 mysql 和 minio 创建 SSD 云盘。
您可以在阿里云上尝试一下单独部署最新版本 Kubeflow Pipelines。 前提条件 您需要安装  kustomize 在 Linux 和 Mac OS 环境,可以执行 opsys=linux # or darwin, or windows curl -s https://api.github.com/repos/kubernetes-sigs/kustomize/releases/latest |\ grep browser_download |\ grep $opsys |\ cut -d '"' -f 4 |\ xargs curl -O -L mv kustomize_*_${opsys}_amd64 /usr/bin/kustomize chmod u+x /usr/bin/kustomize 在 Windows 环境,可以下载  kustomize_2.0.3_windows_amd64.exe 在阿里云容器服务创建 Kubernetes 集群, 可以参考  文档 部署过程 通过 ssh 访问 Kubernetes 集群,具体方式可以参考 文档 下载源代码 yum install -y git git clone --recursive https://github.com/aliyunContainerService/kubeflow-aliyun 安全配置 3.1 配置 TLS 证书。如果没有 TLS 证书,可以通过下列命令生成 yum install -y openssl domain="pipelines.kubeflow.org" openssl req -x509 -nodes -days 365 -newkey rsa:2048 -keyout kubeflow-aliyun/overlays/ack-auto-clouddisk/tls.key -out kubeflow-aliyun/overlays/ack-auto-clouddisk/tls.crt -subj "/CN=$domain/O=$domain" 如果您有TLS证书,请分别将私钥和证书保存到 kubeflow-aliyun/overlays/ack-auto-clouddisk/tls.key 和 kubeflow-aliyun/overlays/ack-auto-clouddisk/tls.crt 下 3.2 配置 admin 的登录密码 yum install -y httpd-tools htpasswd -c kubeflow-aliyun/overlays/ack-auto-clouddisk/auth admin New password: Re-type new password: Adding password for user admin 首先利用 kustomize 生成部署 yaml cd kubeflow-aliyun/ kustomize build overlays/ack-auto-clouddisk > /tmp/ack-auto-clouddisk.yaml 查看所在的 Kubernetes 集群节点所在的地域和可用区,并且根据其所在节点替换可用区,假设您的集群所在可用区为  cn-hangzhou-g , 可以执行下列命令 sed -i.bak 's/regionid: cn-beijing/regionid: cn-hangzhou/g' \ /tmp/ack-auto-clouddisk.yaml sed -i.bak 's/zoneid: cn-beijing-e/zoneid: cn-hangzhou-g/g' \ /tmp/ack-auto-clouddisk.yaml 建议您检查一下 /tmp/ack-auto-clouddisk.yaml 修改是否已经设置 将容器镜像地址由  gcr.io  替换为  registry.aliyuncs.com sed -i.bak 's/gcr.io/registry.aliyuncs.com/g' \ /tmp/ack-auto-clouddisk.yaml 建议您检查一下 /tmp/ack-auto-clouddisk.yaml 修改是否已经设置 调整使用磁盘空间大小, 比如需要调整磁盘空间为 200G sed -i.bak 's/storage: 100Gi/storage: 200Gi/g' \ /tmp/ack-auto-clouddisk.yaml 验证 pipelines 的 yaml 文件 kubectl create --validate=true --dry-run=true -f /tmp/ack-auto-clouddisk.yaml 利用 kubectl 部署 pipelines kubectl create -f /tmp/ack-auto-clouddisk.yaml 查看访问 pipelines 的方式,我们通过 ingress 暴露 pipelines 服务,在本例子中,访问 IP 是 112.124.193.271。而 Pipelines 管理控制台的链接是:  https://112.124.193.271/pipeline/ kubectl get ing -n kubeflow NAME HOSTS ADDRESS PORTS AGE ml-pipeline-ui * 112.124.193.271 80, 443 11m 访问 pipelines 管理控制台 如果使用自签发证书,会提示此链接非私人链接,请点击显示详细信息, 并点击访问此网站。 请输入步骤 2.2 中的用户名 admin 和设定的密码。 这时就可以使用 pipelines 管理和运行训练任务了。 Q&A 为什么这里要使用阿里云的 SSD 云盘? 这是由于阿里云的 SSD 云盘可以设置定期的自动备份,保证 pipelines 中的元数据不会丢失。 如何进行云盘备份? 如果您想备份云盘的内容,可以为云盘  手动创建快照  或者  为硬盘设置自动快照策略  按时自动创建快照。 如何清理 Kubeflow Piplines 部署? 这里的清理工作分为两个部分: 删除 Kubeflow Pipelines 的组件 kubectl delete -f /tmp/ack-auto-clouddisk.yaml 通过 释放云盘 分别释放 mysql 和 minio 存储对应的两个云盘 如何使用现有云盘作为数据库存储,而避免自动创建云盘? 请参考 文档 总结 本文为您初步介绍了 Kubeflow Pipelines 的背景和其所要解决的问题,以及如何在阿里云上通过 Kustomize 快速构建一套服务于机器学习的 Kubeflow Pipelines, 后续我们会分享如何利用 Kubeflow Pipelines 开发一个完整的机器学习流程。
来源:OSCHINA
发布时间:2019-07-23 17:53:00
本文作者:HelloDeveloper 在 7 月 2 号由百度开发者中心、百度开放云联合举办的第 64 期“百度开放云移动游戏和直播技术解读”沙龙上,来自百度的高级产品经理鲁玮,介绍了百度开放云在移动游戏方面的整体解决方案,并就相关实际案例给出具体讲解。 演讲者简介: 鲁玮,百度高级产品经理,2015 年加入百度云计算事业部,现作为百度开放云核心产品“计算与网络产品线”的产品负责人,领导、推动了云服务器、专属机、虚拟私有网络、VPN 服务、专线、弹性 IP 的产品化,实现了百度开放云相关产品从无到有,从有到优的明显提升。 一、移动游戏行业的发展现状 首先,鲁玮老师介绍了近年来中国移动游戏发展的现状,从 2008 年到 2015 年,大概 8 年的时间里,游戏收入从 185 亿增长到 1400 亿,从各个细分领域里的收入增速和发展趋势来看,排名前三的分别是手游、端游和页游。 近几年来,手游经过了爆发式的增长,2015 年的时候,手游的增长率达到 87.2%,手游已经变成了游戏行业里面最主要的而且是发展速度最快的方向。对于移动发行和移动应用来讲,游戏一直都是最主要的垂直模块,移动游戏也已变成了移动互联网里面变现最快的领域。 二、手游发展趋势-向中重度方向发展 手游发展到今天,从刚开始的休闲游戏到卡牌、MMO、MOBA 等,手游也在向中重度方向发展。归结原因,第一点是技术层面,移动处理器和 GPU 的快速发展会保证游戏跑得更加顺畅,重度游戏不会像原来那么卡顿;精细化的 3D 和情景式的游戏,对用户黏性很大,而且开发难度越来越低,这是手游往中重度发展的原因。另一个是运营层面,不能光靠游戏的长期下载量,要让用户沉浸在游戏里的时间更长,而最早出的游戏是偏休闲类的,比如斗地主、连连看,这种相对黏性较低,因为缺少互动模块,它会很容易流失掉用户,所以需要提高互动(帮会、聊天等模块)吸引用户,来产生更多的黏性。另外一个重要原因,这些玩家会付费购买道具来提升自己的级别,支付意愿较明显,所以中重度手游变现能力非常强。 三、移动游戏在部署和运行过程中遇到的痛点 游戏作为一个移动行业里面变现最快的行业,部署和运行过程中肯定会遇到很多问题。 首先手游生命周期短,购买物理资源浪费严重。手游对于其它 APP 来讲生命周期较短,很多游戏的厂商为了支撑游戏峰值业务,购买大量的硬件,而 90% 的情况下不能利用,导致成本和收益不成正比。 第二,流量峰值无法预测,已有 IT 资源无法支撑。很多时候要进行活动大推,或者由于社会热点(影视剧、网络小说)等因素导致游戏受到关注,从而引入大量玩家。这种情况下,原来的服务集群的计算性能无法支撑新涌入的玩家,所以搞活动、大推的时候,流量无法很好的预估。原来游戏部署在 IDC 时会选用物理机,但是物理机宕机恢复时间非常长,至少需要 30 分钟,这种游戏体验对玩家来讲很难接受,它会导致用户大量流失,而且这种传统的架构系统也是基于烟囱式的单点上面部署 OS、搭建应用服务,扩展性很差。 另外,中小游戏厂商使用 IDC,本身其防攻击能力很弱,所以会经常受到攻击,严重影响游戏收入。 四、百度开放云移动游戏解决方案介绍 下面我们从百度开放云游戏客户的实际案例中,分享不同游戏类型的架构解决方案。 卡牌类型手游 这个是卡牌类游戏的解决方案,通过百度开放云高性能的对象存储(BOS)和内容分发网络(CDN)把游戏安装包推送给玩家,玩家登录游戏时,经过云安全(BSS)的严格审查。另外游戏服运行在高性能云服务器(BCC)上,每个 BCC 配置一个单独的高性能数据库(RDS),这种方案大大提高了整个游戏系统的稳定性。通过支付服连接各个游戏渠道,保证了不同渠道引入的玩家购买道具等付费行为,支付数据库也记录了交易信息, 方便游戏 CP 和运营商对玩家的支付行为再进行二次分析。 社交类型手游 这个就是偏向于社交类游戏的解决方案,通过游戏加速服务系统将游戏安装包发送到手机端。玩家经过 BSS 的安全检测后,接入到登录服。因为游戏系统相对比较大,会有游戏服的资源管理、版本管理等模块。游戏大厅,类似于传统棋牌大厅的形式,每一个游戏服里有多个频道,每一个频道有多个房间。这类游戏的周边系统会把各个频道里边的聊天记录保存,同时因为大家在这里要排名,那排名也会有个专门的服务系统;通过跨区接入功能,也实现了玩家跨区的游戏 PK;另外为了增强游戏的流畅度,系统 DB 之前也会有缓存层。同时所有系统模块都会由智能云监控(BCM)来监测业务运行状况,包括通用运维指标、游戏程序等,游戏 CP 根据业务场景自定义设置报警规则,及时发现、处理宕机、业务性能压力过大的风险。 MOBA类型手游 这个是 MOBA 类型游戏的解决方案,MOBA 类型游戏对实时战斗的要求极强,但由于玩家位置不同、接入的运营商不同,导致南方和北方玩家访问质量不一致,游戏体验很差,所以百度开放云推荐此类游戏在南方和北方分别部署游戏集群。根据用户的不同位置,通过智能 DNS 接入最佳机房,实现用户更流畅的游戏体验;基于百度雄厚的基础网络资源,使用专线打通北京和广州区域机房,实现数据高速的同步和备份;另外游戏 CP 使用百度开放云业界领先的大数据服务,实现对玩家日志信息、玩家支付信息的精细分析,为游戏精细化运营提供宝贵数据。 五、移动游戏客户案例说明 沙巴克传奇,是今年盛大游戏重磅推出的 MMORPG 类型的手游,沙巴克传奇对服务器性能、网络安全的要求非常高。盛大游戏不仅需要高 PPS 转发的服务器、数据中心内网之间高速连通,还要求资源独享、灵活计费和完整的数据分析等能力。百度基于高性能的内核优化技术、自建的高质量 BGP 带宽、高性能的计算集群系统、超强的大数据服务,满足了盛大游戏对业务系统的严格要求。 在这个游戏里,不管前期测试、大推阶段还是后期运行管理,我们做了很多事情。沙巴克传奇封测阶段,我们给了很多支持与建议,包括定向内核优化、高可靠和弹性架构方案的推荐、结合上线节奏分配资源专区等。游戏大推阶段,我们提供超高性能的抗 D 服务、网络优化型云服务器、7*24 小时专人运维、游戏驻场支持、全国网络质量的实时监控和预测。后期运维阶段,我们也会定期的安全巡检,给出资源的生命周期管理的建议等。百度开放云提供了一整套完美的游戏解决方案,也帮助了沙巴克传奇实现了平稳的上线和火爆的大推,满足了沙巴克传奇游戏对性能、稳定性、安全等方面的高要求。 原文链接地址: https://developer.baidu.com/topic/show/290237
来源:OSCHINA
发布时间:2019-07-23 17:24:00
本文作者:HelloDeveloper 在 1 月 16 日,由百度开发中心和 InfoQ 联合主办的“纵谈前端接入技术、SEO 和安全运维”主题沙龙活动中,来自百度开发者中心的资深运维工程师们热情洋溢的分享了百度在前端技术、搜索速度优化和全站使用 HTTPS 技术的进展及成果,以及百度在这些方面有哪些宝贵经验可供参考的。演讲嘉宾分别为百度 Golang 委员会成员陶春华、专注于网页搜索无线访问速度的工程师许霞,和处理网页搜索可达性、安全搜索等方向事务的主要技术负责人陈曦洋。 Go 语言在 Baidu Front-End 方面的应用实践 Go 语言的广泛流行取决于部署简单、并发性好、良好的语言设计,以及执行性能好。这也是在重写百度前端这一项目上为什么考虑选用 Go 语言的原因所在。陶春华老师介绍说,促使重写 Baidu Front-End 的诱因主要基于以下三点:一是修改成本高。事件驱动的编程模型本身的编码和调试难度都很大;C 语言本身的难度和开发效率有很多限制。二是配置管理方式落后。为单产品线设计,无法支持平台化要求;配置变更(修改、重载、验证)能力差。三是变更和稳定性的矛盾。例如程序出 core 也是比较头疼的事情。 在此前提之下,团队决定使用 Go 语言来重写前端,但是这里也遇到了一些问题,那就是 GC(Gabage Collection)本身难以避免的时间延迟。BFE 的需求是要在 1ms 以内,最大不超过 10ms,一旦超过这个平均值,那么用户体验将大打折扣。而 Go-BFE 实测 100 万连接,400ms GC延迟。这就需要不断的对 GC 进行优化。 在这里陶老师也介绍了两种优化思路,第一个常见的方法就是将扫描的小对象合并成大对象,利用 Array 来合并一组对象。第二种方法精算性更高一点,可以把消耗内存较多的内容放到 C 里面,因为 Go 语言有一个 CGO 接口,直接通过 Go 调到 C 可以解决这个问题,只不过代价比较大。但是,问题和方案永远是相生相伴的。用 Array 技术重写网络库,所有的 BFE 将永远用 Array 来写,理论上可行。这里的问题又来了,第一风险太大,第二如果 Go 语言升级了,还能不能继续使用下去。 陶老师在这里介绍的解决方法叫做车轮大战。即,在一组工作进程中,进程和服务是等价的,某一个进程跟服务运作到一定时间之后关闭GC,让它休息,第二个进程代替它服务,以此轮换,构成一个车轮大战的局面。如果在不能直接解决GC问题的时候直接关掉服务,然后绕过它。这基本的方案思路也就是关闭继承多进程的轮转战。(如上图) 搜索速度优化的前进之路 在整个百度接入服务里,百度搜索一直秉承提供最基础的三个保障,那就是安全、快速、可靠。许霞首先介绍说,在对速度进行度量之前,先要对数据检测、收集。对客户端数据监测的特点是:可以检测任何对象,成本高,并且监测的指标很固定。JS 埋点检测数据的特点是:可以检测任何指标,甚至可以检测每一条结果的速度。第三方数据检测的特点是:可以定制,并且有一定的海外监测能力,但成本高。 收集数据的意义在于可以很清晰的了解掌握用户的搜索习惯,这对 PV、UV 以及变现收入有很大影响。那么如何贴切搜索引擎的特点做搜索速度的优化?通过三个方面:接入质量提升、后端处理优化和前端渲染优化。接入质量提升主要有两个考察因素:延迟和带宽,对应的也就是优化 RTT 和传输效率。 后端优化其实就是整个搜索引擎的优化了,分为缓存优化和检索优化。缓存优化最基础的方式就是进入、淘汰机制等等,保证淘汰机制是最合理的。检索优化,则需要对硬件以及硬件方案的选择做一些深入考虑。在前端渲染优化方面,除了考虑节省时间之外还要考虑怎样让它定性化。 对优化做决定性决策只是其中的一种方法,还有更聪明的创新方法,那就是关于无线技术。这里面所涉及的内容包括手机终端、机站以及 IP 网络,传输速度当然是跟这三者有密不可分关系的。机站会根据自己能获得多少收益来处理用户的请求,尽量会缩小头部信息,进行一定程度的数据压缩。手机跟机站之间建立连接以维持这种连接关系。但电耗大是很关键的问题。百度搜索做了维持连接的一些机制,当用户页面空闲很长时间或者放在后台,就可以减少电量的消耗。(如上图) 全站 HTTPS 能否确保网站彻底安全? 2015 年 3 月,百度搜索成为国内首家完成全站 HTTPS 改造的大型站点;且目前来看,全站 HTTPS 已经成为百度产品的首要标准;同时,统一接入平台也大幅提升了 HTTPS 的接入效率和性能。陈曦洋老师在开讲前是这样介绍大背景的。全站 HTTPS 的原因是为了让用户保持良好的使用体验,解决反馈较多的劫持和隐私泄露等问题。这些问题的具体 case,包括页面被加上 URL 参数,不停刷新;页面被 DNS 劫持到其他网站;用户手机号码遭泄漏;白页,搜索功能异常等等。正是出于对用户数据的安全保密,维护网站正常运作的考量,百度专门成立了由百度搜索和运维部组成的 HTTPS-SUPPORT 团队,对 HTTPS 进行深入研究,提供完整的服务,保障用户正常访问百度原始产品。 陈曦洋老师在这里详细介绍了全站 HTTPS 改造的成本,这也是很多人都比较关心的焦点问题,这不仅涉及到证书的部署,还会涉及到激增的计算量,需要多次协商和握手,而用户端搜索的延迟将会给 HTTPS 改造需要解决的问题。除此以外,对于一个大型网站而言,架构如何解决多业务部署HTTPS的问题,巨大的页面和模板数量,以及如何解决实际部署中的各种问题,让用户无损 / 平滑的完成切换,其实是更具有挑战性的工作。 计算性能涉及到密钥(证书)的长度,1024 和 2048 位在性能有什么差别呢?原来使用 HTTP 协议的时候,假设 cps 可以达到 2w+,而转换成 HTTPS 之后,cps 只能达到 2-3 千;在访问速度方面,使用了 HTTPS 之后,不做任何优化,访问百度的速度可能会恶化 250-500ms, 一些设计比较差的页面可能会恶化 500-1200ms;在架构和产品成本方面,对于百度这样的综合性网站,牵一发而动全身,仅仅是在页面形式上就要改大量的模板,成本相当大。 那么有没有可选的优化方案呢?陈老师认为,性能优化上优先使用 ECC。这里使用 ECC 密钥长度大小要比 RSA 和 DH 密钥长度短。在硬件的优化上则可以使用硬件加速卡,可以做 TLS 的远程卸载 (小型站点在不面对大量的恶意请求时 完全可以通过纯软件卸载, 只需要保证连接复用率)。在访问速度上的优化上,通过复用连接和协议优化可以尽量减少握手次数,就可以让它接近于原始 HTTP 的性能。怎么去减少握手次数?比如 Session cache 和 Session ticket 可以极大的减少用户在一定时间内再次访问时的计算开销,而 HSTS 能在浏览器内部完成 HTTP 到 HTTPS 的跳转,不再经过一次网络传输和浏览器开销。另外还可以用 SPDY-HTTP2 方案,优点是基于单连接,能进一步提升连接复用比例,协议支持 header 压缩,在无线网络下有重要意义,这些都可以提高访问速度。 除了对协议层进行优化之外,也可以在应用层做些优化,预连接就是一个很好的优化方案。比如在网页端或者客户端,用户发起访问请求之前提前把这个握手过程完成,减少延迟,这一点也很重要。另外陈老师建议站点在发展到一定规模时一定要做整体的接入规划,控制域名数量,一些服务需要变成公共的,比如图片,静态资源的存储和访问。 在最后,陈老师也回答了大家普遍比较关心的问题,那就是使用 HTTPS 就代表着绝对安全吗?事实上并没有绝对地安全,代码是人写的,很多问题都是实际的实现上或者依赖的其他环境上出现了漏洞,OpenSSL HeartBlood 就是最典型的案例,甚至连随机数的生成和一些加密算法上也可能有人为埋下的漏洞,CDN 回源这样的路径很多情况下也是使用的 HTTP。百度使用 HTTPS 只能保护用户在浏览百度产品的时候的安全,但是很多手机号的泄露是第三方站点导致的 (它们会通过非法渠道购买识别用户手机号的服务),这个问题并不能通过百度的 HTTPS 解决。但是相对于 HTTP,HTTPS 的安全防范性能更高,增加了坏人的做恶难度。 原文链接地址: https://developer.baidu.com/topic/show/290236
来源:OSCHINA
发布时间:2019-07-23 17:20:00
本文作者:y****n 自然语言处理(NLP)素有“人工智能皇冠上的明珠”盛誉,这也意味着语言与知识等认知层面的技术突破将进一步促进AI深入发展。 8月25日,以“掌握知识、理解语言、拥有智能” 为主题的百度大脑语言与知识技术峰会举行,百度CTO王海峰发表主旨演讲,解读百度语言与知识技术的发展历程与最新成果,与产学研各界分享技术及产业发展趋势和展望,百度集团副总裁吴甜和百度技术委员会主席吴华分别发布百度语言与知识技术系列产品和数据集共建计划,重磅推出5款产品 的新发布,全面加速AI技术大规模应用。这是一场凝聚了百度在语言与知识领域十年技术积累和产业实践的盛会,必将带来深远影响。 十年: 开拓者、深耕者、引领者 语言与知识技术是人工智能认知能力的核心。2010年,百度成立自然语言处理部,在前瞻技术与产业格局上不断引领、创新,十年间已成为中国NLP发展的一面旗帜。 峰会上,王海峰回顾,“在百度语言与知识技术的布局和发展中,我们始终在注意把握两个趋势,即技术发展趋势和产业发展趋势 ,并力争引领趋势。”纵览百度语言与知识技术发展历程, 从研究方法、研究对象、研究方向、产业应用 等各个层面,布局完整,不断打磨成熟,始终与应用的发展趋势、需求一脉相承,与产业接轨。 十年来,百度大脑语言与知识技术成果丰硕,获得包括国家科技进步奖在内的20多个奖项,30多项国际竞赛冠军,发表学术论文超过300篇,申请专利2000多项 。技术不断突破创新的同时,也在产品上创新探索,同时将领先的技术输出给开发者与合作伙伴,提升各行业智能化水平。 全面分享语言与知识技术成果 王海峰全面分享了百度语言与知识技术完整布局和最新成果。首先,知识图谱是机器认知世界的重要基础,百度打造了世界上最大规模知识图谱, 拥有超过50亿实体和5500亿事实 ,并在不断演进和更新。百度知识图谱应用于各行各业,每天的调用次数超过400亿次 。 其次,在融入知识的基础上,语言理解能力不断增强。2019年3月,百度提出知识增强的语义理解框架ERNIE,在深度学习的基础上融入知识,同时具备持续学习能力,曾一举登顶全球权威数据集GLUE榜单,首次突破90分大关,刷新榜单历史 。基于知识图谱和语义表示,突破了阅读理解、对话理解以及跨模态深度语义理解等技术。第三,语言生成是语言与知识技术中的重要组成部分。基于预训练技术的成功经验,百度提出基于多流机制的语言生成预训练技术,兼顾词、短语等不同粒度的语义信息,显著提升生成效果。百度也探索了多文档摘要生成,通过图结构语义表示引入篇章知识,在单文档和多文档摘要生成效果都有提升。 应用系统层面,对话系统和机器翻译等成绩卓著。 百度提出了知识图谱驱动的对话控制技术,以及首个基于隐空间的大规模开放域对话模型PLATO等 ,并推出智能对话定制和服务平台UNIT,帮助开发者高效构建智能对话系统,实现规模化应用。百度翻译支持200多种语言,每天响应超过千亿字符的翻译请求,支持超过40多万家第三方应用 ,技术上,提出了多智能体联合学习、基于语义单元的同传模型、稀缺语种分组混合训练算法等。 百度大脑语言与知识技术的持续探索和创新取得了令业界瞩目的成绩,同时这些技术以平台化的方式输出,赋能千行万业,持续提升产业智能化水平。 重磅推出5款产品的 新发布、2大计划 王海峰首次发布了百度大脑语言与知识产品全景图。百度集团副总裁吴甜接续发布语义理解技术与平台文心、智能文档分析平台TextMind和AI同传会议解决方案 3大新产品,同时发布了6项升级,包括智能创作平台的3个场景方案、以及智能对话定制与服务平台UNIT的3项全新升级。吴甜表示,“我们一直致力于将语言与知识技术凝聚成一系列技术平台和产品,在应用中产生大量价值,为广大开发者和产业实践者提供以语言与知识技术为核心驱动的系列产品。” 百度推出的 语义理解技术与平台文心 ,基于深度学习平台飞桨打造,依托领先的语义理解核心技术,集成优秀的预训练模型、全面的NLP算法集、端到端开发套件和平台,提供一站式NLP开发与服务,让开发者更简单、高效地定制企业级NLP模型。文心经过了大量真实应用场景的淬炼,具备优秀的工业级落地实力。 全新发布的智能文档分析平台TextMind ,基于OCR、NLP技术,以文档解析为核心能力,支持文档对比与文档审核,具备“多快好省”的核心优势,促进企业办公智能升级。 百度大脑智能创作平台针对媒体应用场景再升级,全新推出智能策划、智能采编、智能审校三大媒体场景方案,进一步助力媒体人更快、更好地创作,可谓切中媒体人的“痛点”。 智能对话定制与服务平台UNIT 升级3大特性:更智能的任务式对话理解、极致便捷的表格问答和融合通用的新对话引擎。此次UNIT全新升级的三大能力,将进一步降低任务式对话、智能问答的定制成本,并融合通用对话能力,提升交互体验。 全新发布的AI同传会议解决方案 ,覆盖会议全场景、全流程,旨在打造用户随身的“会议同传专家”。吴甜现场展示了如何只用一台电脑和一部手机快速搭建一套同传服务,只需点点鼠标、打几个字,就能快速获得专业的同传服务。 据匮乏、算力不足历来是语言与知识技术研发中面临的瓶颈。为突破瓶颈,百度联合中国计算机学会、中国中文信息学会发起中文自然语言处理数据共建计划——千言 ,解决数据稀缺问题。千言一期由来自国内11家高校和企业的数据资源研发者共同建设,已涵盖开放域对话、阅读理解等7大任务,20余个中文开源数据集。百度技术委员会主席吴华表示,“未来,我们希望有更多的数据集作者能够参与共建千言,共同推动中文信息处理技术的进步,建设世界范围的中文信息处理影响力。 我们计划在未来3年,面向20多个任务,收集和建设不少于100个中文自然语言处理数据集,覆盖语言与知识技术全部领域。” 吴华还发布了百度语言与知识技术算力共享计划,通过百度AI Studio平台提供算力支持,让广大开发者破除算力桎梏,专注于技术创新。 十年征程,百度语言与知识技术发展历程中培养、吸引了大量全球顶尖人才。会上,百度推出以王海峰为代表的百度NLP“十年十人”,十年坚守,不忘初心,秉持“技术信仰”,勇攀技术高峰,矢志不渝致力于让机器更好地理解世界、更好地服务于人。 正如王海峰所言,“我们致力于更好地与学术界、产业界携手,推动语言与知识技术发展,进而推动人工智能技术持续进步,为产业智能升级、社会经济高质量发展贡献力量。我们对未来充满信心,坚持研究和发展让机器掌握知识、理解语言、拥有智能,继续突破和创新,为技术和社会进步做出更大贡献。” 原文链接地址: https://developer.baidu.com/topic/show/291190
来源:OSCHINA
发布时间:2020-08-26 10:22:00
本文作者:y****n 要说生活里最常见、最便民的AI应用技术,OCR(Optical Character Recognition,光学字符识别)当属其中之一。寻常到日常办理各种业务时的身份证识别,前沿到自动驾驶车辆的路牌识别,都少不了它的加持。作为一名开发者,各种OCR相关的需求自然也少不了:卡证识别、票据识别、汽车场景、教育场景文字识别…… OCR领域向来开源repo比较少,大部分核心算法用在了商业化产品。今年算是OCR开源领域的丰收年,chineseocr_lite,easyocr,以及百度飞桨推出的PaddleOCR先后横空出世。 确实喜大普奔 对于OCR方向开发者而言,开源repo最吸引人的莫过于: ① 高质量的预训练模型 ② 简单易上手的训练代码 ③ 好用无坑的部署能力 简单对比一下目前主流OCR方向开源repo的核心能力对于 语种方面 ,easyOCR的优势在于多语言支持,非常适合有小语种需求的开发者; 从预训练模型 来看,easyOCR目前暂无超轻量模型,chineseocr_lite最新的模型是10M左右,而PaddleOCR提供的8.6M是目前业界已知最轻量的 ; 对于部署方面 ,easyOCR模型较大不适合端侧部署,Chineseocr_lite和PaddleOCR都具备端侧部署能力; 对于自定义训练 ,实际业务场景中,预训练模型往往不能满足需求,对于自定义训练和模型Finetuning,目前只有PaddleOCR支持 ; PaddleOCR项目地址: https://github.com/PaddlePaddle/PaddleOCR PaddleOCR 8.6M超轻量模型,支持自定义训练、丰富的部署方式(覆盖服务器端、移动端/嵌入式端(apk/sdk)多场景需求)。提供的超级开源开发者大礼包,无疑让开发者大呼过瘾 ,看一下repo中提供的教程文档,真心全覆盖。高质量的内容也换来了开发者的广泛认可,GitHub Trending 第一,Star数量已经超过2.5k。 这仅仅是开始。。。 随着大量用户涌入,也实实在在提出了很多业务场景常见的问题,比如: 能否解决自然场景任意形状文本检测问题? 文字识别结果能否通过语义信息自动纠正? 既然开发者有需求,百度飞桨也是诚意满满! 百度自研SAST、SRN 两大SOTA算法开源啦! 核心信息先睹为快: 面向自然场景任意形状文字检测问题,开源ACM Multimedia 2019上发表的SAST(A Single-Shot Arbitrarily-Shaped Text Detector based on,Context Attended Multi-Task Learning)算法,在多个公开数据集(包括SCUT-CTW1500,Total-Text,ICDAR15 和 MLT),准确度取得了SOTA或可比的结果,速度上位列领先行列。 面向场景文本图像中兼顾视觉信息和语义信息的需求,开源CVPR2020中发表的SRN(Towards Accurate Scene Text Recognition with Semantic Reasoning Networks )算法,在包括ICDAR13、ICDAR15,IIIT5K,SVT,SVTP,CUTE80数据集,准确度取得了SOTA或可比的结果。 开源算法详细解读 01 单阶段任意形态文字检测器-SAST(ACM MM2019) 论文地址:https://arxiv.org/abs/1908.05498第一列为语义分割图,黄色框标记的为较长文字分割响应断裂的情况;第二列为SAST利用Pixel-to-Quad思想对实例分割的处理结果,相同颜色为同一个文字实例;第三列红色框为最终检测结果,蓝色为真值,青色为EAST算法检测结果。 目前业界解决任意形态文字检测问题的主流思路有两种: 一种基于Mask-RCNN思想的自顶向下的检测方法,例如LOMO、PMTD等; 另外一种是基于语义分割的自底向上的检测方法,例如TextField、TextMontain 等。 自顶而下的方法往往由于算法级联的模块比较多、特征抽取比较复杂导致实际使用效率没法得到很好保证;语义分割的方法由于缺乏实例(instance)的先验,在面临距离较近的文字条难以分割开、过长的文本条则容易出现响应不连续等问题。 SAST (“A Single-Shot Arbitrarily-Shaped Text Detector based on Context Attended Multi-Task Learning”) 收录于ACM Multimedia 2019,是百度研发的面向自然场景任意形状文字检测问题,兼顾效率和准确率的解决方案。 SAST使用多任务学习对文字中心线区域进行语分割的同时学习了文字实例的多种几何信息,进而实现文字的实例分割和多边形表达的重建。该方法的整体算法流程,如图1所示: Ⅰ 首先,通过多任务学习方法学习文字实例的多种几何信息,包括文字条中心线响应图 (Text Center Line, TCL),中心线像素与四角点偏移量 (Text Vertex Offset, TVO),中心线像素与文字中心点偏移量 (Text Center Offset, TCO) 和中心线到文字上下边界偏移量 (Text Border Offset, TBO); Ⅱ 其次,使用Pixel-to-Quad方法对TCL进行实例分割,该方法结合了高层检测信息和底层分割信息,具体过程如图2虚线框中所示。 Ⅲ 最后,在实例分割的基础上,针对每个文字实例结合TBO信息,即可恢复出任意形状文字的几何表达。 论文中对SAST在多个公开数据集上进行了效果验证,包括SCUT-CTW1500,Total-Text,ICDAR15 和 MLT数据集,准确度上取得了SOTA或可比的结果,速度上位列领先行列。此次SAST开源工作中,通过对模型的Backbone和训练数据做了适当的升级和调整, 在icdar2015数据集上hmean达到87.33%,在弯曲文本数据集total-text上hmean达到84.03%。 02 利用语义推理网络强化的场景文字识别器-SRN(CVPR2020) 论文地址:https://arxiv.org/abs/2003.12294(a)是较难识别的场景文本图像,(b)是从(a)中图像里分离抽取出的字符图像,(c)是(a)中图像对应的语义内容。 场景文本图像包含两种层次的内容:视觉信息和语义信息。近年来致力于提高识别性能的工作,大多从提取鲁棒且有效的视觉特征的视角出发,例如升级模型Backbone,增加矫正模块,校准Attention机制等,往往忽视了改进语义特征。场景文字识别不单依赖视觉感知信息,也依赖高层次的语义信息的认知理解,例如图3所示 ,仅依靠视觉信息是很难准确识别分离抽取出的字符图像,尤其是(b)中红框标出的字符;相反地,结合整个单词完整语义信息进行推理的时候,我们是可以很容易识别出完整单词内容的。 为了解决如下主流Attention-based隐含式语义信息建模的方法缺陷: 1)仅仅感知了历史时刻的语义信息,而无法获取其他时刻的语义信息; 2)如果较早时刻解码出的错误字符,会为余下时刻的解码传递错误的语义信息,导致误差积累效应; 3)串行的解码模式是相对低效的(特别是在模型预测的环节) 百度在CVPR 2020录用工作SRN(Towards Accurate Scene Text Recognition with Semantic Reasoning Networks )中借鉴Transformer和NLP预训练模型思想提出了一种高效的全局并行语义推理模块GSRM,其信息传递方式采用如图 4(b)所示方式。 SRN是端到端可训练的场景文字识别网络,由四部分组成:基础网络Backbone、并行的视觉特诊提取模块(PVAM)、全局语义推理模块(GSRM) 和视觉语义融合的解码器(VSFD)。给定一张输入的文本图像,基于ResNet50 + Transformer unit的Backbone从中提取出视觉2D feature map V ;之后PVAM会针对每个目标字符获取其相应的视觉特征G ;GSRM会基于视觉特征G 获取全局语义信息,并转化为每个目标字符的语义特征S ;最后VSFD融合对齐的视觉特征和语义特征,预测出相应字符。在训练阶段和推断阶段,每个序列中各个字符之间是并行。 SRN在多个公开数据集上进行了效果验证,包括ICDAR13、ICDAR15,IIIT5K,SVT,SVTP,CUTE80数据集,在准确度上取得了SOTA或者可比的结果 。同时,也在中文长词数据集合TRW上与主流方法做了精度对比,证明了该方法对于中文的适用性。图 6中展示了语义推理模块的使用与否在中英文上的可视化对比效果。加了GSRM模块后,能将一些不符合语义逻辑的单字识别结果进行纠错。 参考DTRB文字识别训练和评估流程,使用MJSynth和SynthText两个文字识别数据集训练,在IIIT, SVT, IC03, IC13, IC15, SVTP, CUTE数据集上进行评估,算法效果如下:可以看到SRN检测算法指标明显优于其它算法,效果提升明显。而且值得一提的是,此次PaddleOCR开源SRN工作中,对训练方法做了适当的优化,采用一步到位端到端的训练方式,原始论文使用两阶段训练平均精度为89.74%,PaddleOCR中使用one-stage训练,平均精度为88.33%。精度虽然略有下降,但是训练效率和实用性明显增加。 开源数据集建设 除了开源自研算法,百度也一直致力于推动开源数据集的建设。ICDAR“Robust Reading Competitions”竞赛是评估自然场景/网络图片/复杂视频文本提取与智能识别新技术进展的权威国际赛事及评测标准,竞赛中涌现出诸多方法持续推动业界新技术的创新与应用。 作为ICDAR 2019 Robust Reading Competition竞赛主要组织者之一,百度联合学术界共同发布了两项极具挑战的竞赛任务, ICDAR 2019-LSVT ( L arge-scale S treet V iew T ext with Partial Labeling, 弱标注大规模街景文字识别竞赛)、 ICDAR 2019-ArT ( Ar bitrary-Shaped T ext, 任意形状场景文字识别竞赛)。 ICDAR 2019-LSVT竞赛数据聚焦探索大规模数据场景下深度学习文字识别能力极限,是业界最大的中文场景OCR集合。ICDAR2019-LSVT数据集源于百度真实应用场景,作为首个提出弱标注数据的场景文字数据集,包括精标注5万张街景图像,40万张弱标注图像,总计45万,数据量是现有公开数据集(ICDAR 2017、ICPR 2018等)的14倍以上。场景文字识别具有广泛应用场景,例如:拍照翻译、图像检索、街景地标识别、室外场景理解等。 ICDAR2019-ArT竞赛数据总计10176张,是业界最大的任意形状场景文字集合,聚焦推动自然场景下任意形状文字检测识别能力新突破。 PaddleOCR开源的超轻量和通用版中英文模型,训练数据组成中的中文真实数据集,主要就是上述开源的LSVT数据集,此外,本次SAST算法开源模型total-text指标超过论文指标约4%,主要原因也是由于加入了ArT数据集进行了优化。以上数据集也已经在PaddleOCR中开源了,欢迎使用: https://github.com/PaddlePaddle/PaddleOCR/blob/develop/doc/doc_ch/datasets.md 原文链接地址: https://developer.baidu.com/topic/show/291177
来源:OSCHINA
发布时间:2020-08-25 09:20:00
本文作者:y****n 8月21日,百度在沧州开放Apollo Go自动驾驶出租车服务,沧州市民通过百度地图,即可一键呼叫免费搭乘体验,沧州由此成为中国第一个可以在主城区打到Robotaxi的城市。这也是继今年4月在长沙全面开放之后, Apollo在第二个城市上线常态化的打车服务,这意味着Apollo正在加速规模化部署,百度无人车服务Apollo Go迈入多地运营的全新阶段。 百度此次在沧州开启的Apollo Go Robotaxi服务,由Apollo和生态伙伴云图科技联合运营。 行车路线将覆盖高铁站、学校、星级酒店、博物馆、产业园等沧州核心区域。值得一提的是,Apollo 首次尝试将Robotaxi服务深入到高铁站、星级酒店等主城公共区域之间的网约车通行,“驶进”更多普通用户的主流生活场景。 沧州是京津冀城市群和京沪大通道上的重要节点城市,近几年,沧州大力发展智能网联技术,在自动驾驶落地应用方面,已经走在了国内城市的前列。沧州是中国北方第一个开展自动驾驶载人测试的城市,拥有全国第二大智能网联测试路网。 2019年9月,沧州与百度达成自动驾驶、智能交通战略合作。2019年11月,中国首个区级全域自动驾驶可载人测试路网在沧州开放,百度Apollo自动驾驶车队在当地的载人测试也随之开启,而此次Apollo Go Robotaxi在沧州主城区开放,更将加速沧州成为北方的智能网联创新高地。 百度Apollo是全球最大的自动驾驶开放平台,截止目前, 百度Apollo拥有自动驾驶路测牌照数总计超过150张、测试车队规模达到500辆级别、获得全球智能驾驶专利1800件、测试里程总计超过600万公里、实现安全载客超过10万人次。 2019年,百度Apollo与一汽红旗合作的前装量产的Robotaxi亮相长沙开启试运营,今年4月,百度Robotaxi向长沙市民全面开放试乘服务。目前,除了在长沙、沧州外,Apollo也已经在北京、重庆、阳泉等地开展自动驾驶载人测试。 Apollo Go Robotaxi在长沙、沧州多地开启载客试运营,是百度落地自动驾驶技术又一新的里程碑。 基于真实场景的载客运营,将加快自动驾驶的技术迭代和商业探索,加速Apollo实现惠及人人的简单、安全、美好的出行体验。 原文链接地址: https://developer.baidu.com/topic/show/291172
来源:OSCHINA
发布时间:2020-08-24 16:00:00
作者 | 易立  阿里巴巴资深技术专家 导读 :WebAssembly 技术已经走出浏览器,让计算无处不在。本文利用 containerd 的扩展机制,可以为 WebAssembly 应用提供与其他容器应用一致的、抽象的、应用分发、交付和运维模型,可以在 Kubernetes 集群中进行统一调度和管理。 无处不在的 WebAssembly 如果评选 2019 年编程技术的“网红”,无论是前端圈还是后端圈,WebAssembly (WASM) 都绝对能够高票入选。然而,如果评选最被“低估”的技术,我觉得 WebAssembly 也可以轻松入围。 借用伏尔泰曾评价神圣罗马帝国的句式 “既不神圣,也不罗马,更非帝国”,我们也可以说WebAssembly “既不限于 Web,更不是 Assembly(汇编语言)”。 在 2019 年 12 月,万维网联盟 (World Wide Web Consortium  - W3C) 宣布  WebAssembly 核心规范正式成为 Web 标准 ,  这使得 WebAssembly 成为互联网上与 HTML, CSS, and JavaScript 并列的第四种官方语言,可以原生的运行在浏览器上。而更加重要的是,WebAssembly 作为一个安全的、可移植、高效率的虚拟机沙箱,可以在 Internet 的任何地方、任何平台(不同操作系统,不同 CPU 体系架构下)安全运行应用。WebAssembly 已得到了所有主流浏览器厂商的广泛支持(Google Chrome, Microsoft Edge, Apple Safari, Mozilla Firefox 等),然而它的影响已经远超 Web。 WebAssembly 的设计初衷之一是为了解决 JavaScript 的性能问题,使得 Web 网页应用有接近本机原生应用的性能。作为一个通用、开放、高效的底层虚拟机抽象,众多编程语言(如 C/C++, Rust 等)可以将现有应用编译成为 WASM 的目标代码,运行在浏览器中 。这让应用开发技术与运行时技术解耦,极大促进了代码复用。 Mozilla 在 2019 年 3 月推出了 WebAssembly System Interface(WASI),来标准化 WebAssembly 应用与系统资源的交互抽象,比如文件系统访问,内存管理,网络连接等,类似 POSIX 这样的标准 API。WASI 规范大大拓展了 WASM 应用的场景,可以让其可以超越浏览器环境,作为一个独立的虚拟机运行各种类型的应用。同时,平台开发商可以针对具体的操作系统和运行环境提供 WASI 接口不同的实现,可以在不同设备和操作系统上运行跨平台的 WebAssembly 应用。这可以让应用执行与具体平台环境实现解耦。这一切使得“Build Once, Run Anywhere”的理想逐渐形成现实。WASI 的示意图如下所示。2019 年 11 月,为了进一步推动模块化 WebAssembly 生态系统,Mozilla、Fastly、英特尔和红帽公司携手成立了字节码联盟( Bytecode Alliance ),共同领导 WASI 标准、 WebAssembly 运行时、语言工具等工作。 图片来源: https://hacks.mozilla.org/2019/03/standardizing-wasi-a-webassembly-system-interface/ WASM 与容器相爱相杀 WebAssembly 是否会取代容器? 正因为 WebAssembly 所具备的的安全、可移植、高效率,轻量化的特点,非常适于应用安全沙箱场景。WASM 得到了容器、函数计算、IoT / 边缘计算等社区的广泛关注。Docker 创始人 Solomon Hykes 在 WASI 发布之际的一句 Twitter,更是成为了去年容器和 WebAssembly 社区引用频率最高的一句话之一。 Fastly, Cloudflare 等 CDN 厂商基于 WebAssembly 技术实现了更加轻量化的应用安全沙箱,可以在一个进程内部运行多个独立的用户应用。阿里云 CDN 团队 EdgeRoutine 也实现了类似技术。与容器技术相比,WASM 可以实现毫秒级冷启动时间和极低的资源消耗。 原图: https://blog.cloudflare.com/cloud-computing-without-containers/ 当然,世界上没有完美的技术。任何沙箱技术不可能同时满足执行效率、安全隔离性和通用性这三个维度的要求。WASM 在安全隔离和通用性等方面与 Docker Container 等存在差距。虽然如此,我们还是看到了 WebAssembly 技术巨大的潜力。 WebAssembly 容器 我的理解是 WebAssmebly 可以成为一种容器类型,类似 Linux Container 或者 Windows Container 一样。成为一个跨平台的标准应用分发方式和运行时环境。 应用分发 Docker 容器的一个重要贡献是其标准化了容器化应用打包规范 Docker Image,而且它已经成为开放容器计划 (Open Container Initiative - OCI) 的镜像格式标准。Docker 镜像提供了自包含、自描述的镜像格式。它可以将应用以及其依赖的环境信息打包在一起,从而实现应用与运行环境解耦,让容器应用可以轻松运行在从本地开发环境到云端生产环境的不同场景中。并且社区围绕 Docker 镜像构建了繁荣的工具链生态,如 Docker Hub 可以进行应用分发和 CI / CD 协同,Nortary / TUF 项目可以保障应用可信地分发、交付。 对与 WebAssembly,目前社区提供了类似 NPM 的包管理实现 WAPM ,可以较好地支持应用的分发。 为 WebAssembly 应用构建 Docker 镜像,可以实现双赢的局面。 WebAssembly 开发者可以完全复用 Docker/OCI 镜像规范和工具链,进一步简化应用分发和交付。比如,我们可以将 Nginx 的 WASM 镜像作为基础镜像,基于这个镜像可以构建包含不同 Web 内容的应用镜像;我们可以利用 tag 对应用版本进行追踪;利用 Docker Registry 进行应用分发;在这个过程我们还可以进一步利用数字签名来保障安全的软件供应链; Docker 镜像规范支持  Multi-Arch  镜像,可以简化不同 CPU 体系架构(如 x86, ARM, RISC-V 等)的应用镜像的构建与分发。而 WebAssembly 天生具备可移植性,大大简化了跨平台 Docker 应用镜像的构建和分发。参考: 利用 Docker 加速 ARM 容器应用开发和测试流程 。 我提供了一个技术原型 示例项目 ,大家可以参考其中的例子来构建 WASM 容器镜像。由于 WebAssembly 应用采用紧凑的二进制格式,而且没有任何操作系统依赖,WASM 应用可以构建出非常小的容器镜像。大家可以自行感受一下: $ sudo ctr image ls REF TYPE DIGEST SIZE PLATFORMS LABELS docker.io/denverdino/c-http-server-wasm:latest application/vnd.docker.distribution.manifest.v2+json sha256:2efa759f46f901cda2e6a9b4228c423b17a960c06e957964e72c21dc5b42408f 29.2 KiB linux/amd64 - docker.io/denverdino/hellowasm:latest application/vnd.docker.distribution.manifest.v2+json sha256:cadcc8b07eb82b18db2c8f500fa2b11e5ebf2e9054cfa687e4ffe44861860132 8.2 KiB linux/amd64 - docker.io/denverdino/nginxwasm:latest application/vnd.docker.distribution.manifest.v2+json sha256:8735c82524a463b842b7c79f2c1be8094ee1c57cfd34154f68752fbe79c25998 582.7 KiB linux/amd64 - 安全隔离 WebAssembly 的最初设计目标是让应用可以安全运行在浏览器中。WASM 虚拟机提供的 沙箱和内存隔离机制 ,可以有效减少安全攻击面。而当 WebAssembly 走出浏览器,面向更加通用的场景。WASM 也面对更加复杂的安全挑战。 WASI 提供了 基于能力的安全模型 。WASI 应用遵循最小权限原则,应用只能访问其执行所需的确切资源。传统上,如果应用需要打开文件,它会带路径名字符串调用系统操作 open。然后系统调用会检查应用是否具有访问该文件的相关权限,比如 Linux 实现了基于用户/组的权限模型。这样隐式的安全模型,依赖于正确的安全管理配置,比如一旦特权用户执行了一个恶意应用,它就可以访问系统中任意的资源。而对于 WASI 应用而言,如果它需要需要访问指定文件等系统资源,需要从外部显式传入加有权限的文件描述符引用,而不能访问任何其他未授权资源。这中依赖注入的方式可以避免传统安全模型的潜在风险。 一个示意图如下: 原图: https://hacks.mozilla.org/2019/03/standardizing-wasi-a-webassembly-system-interface/ 我们可以看到 WASI 的安全模型与传统操作系统安全模型非常不同,而且还在持续演进中。比如字节码联盟提出了 nanoprocess 来解决应用模块间的安全协同和信任传递。 WebAssembly/WASI 的安全模型依然存在不足,比如: 资源隔离 对于内存资源,WebAssembly 实现了线性内存模型。WebAssembly 应用只能利用索引访问传入的一段逻辑线性内存。而 WASM 虚拟机负责确定内存的实际物理地址,WASM 应用无法获知内存的真实地址,也无法通过越界访问等方式发动攻击。所以理论上,可以对 WASM 应用进行资源容量限制。但是目前部分 WASM 虚拟机还无法对内存进行精确的隔离限制。 对于 CPU 资源,部分的 WASM 虚拟机实现可以对应用使用的 CPU 资源进行计量,但是大多无法实现精确的配额限制、优先级和抢占式调度。 I/O 资源,比如 IOPS 等,WASM 目前完全没有相关的隔离能力。 网络安全 WASI 的 Capability 模型对于文件系统访问相对比较容易保护。但是这个静态的安全模型无法适用于动态的网络应用场景。在微服务架构中,应用经常通过 Service Registry 进行服务发现,为服务的调用者和提供者实现动态的调用绑定。这个语义是无法用静态的 capability 模型描述和注入的。这也导致了 WASI 的网络部分 API 还处于讨论之中。现有的  WASI 网络安全模型 ,以及相关 讨论 。 Linux 操作系统和容器技术已经提供了非常完备的资源隔离和安全隔离实现。与 WebAssembly 结合在一起可以应对不同场景对不同隔离级别的需求。 共享进程资源 - 多个 WASM 应用模块运行在一个 WASM 虚拟机进程内部,依赖 WASM 运行时进行隔离。隔离级别低,控制粒度比较粗,资源开销极小。可以以较小代价保障系统安全。适合受限问题域的应用安全隔离; 独立进程资源 - 不同 WASM 应用模块运行在不同的 WASM 虚拟机进程中,可以复用操作系统的进程级隔离能力,比如 CGroup。此外,还可以利用类似 Kubernetes 中的 Network Policy (网络策略),或者服务网格(如Istio)等技术,对进程的网络访问进行细粒度的控制,甚至实现零信任网络。隔离级别比较高,控制粒度比较细,资源开销适中。可以应用于更加通用的场景。 注:当然利用安全沙箱如虚拟化等技术,结合 WebAssembly,可以进一步最小化安全攻击面,但是 ROI 不高。 调度与编排 在云时代,Kubernetes 已经成为分布式环境下资源调度和应用编排的事实标准。Kubernetes 可以屏蔽底层设施的差异性。可以在同一个 K8s 集群中包含 x86、ARM 等不同体系架构的节点,可以支持 Linux,Windows 等不同的操作系统。Kubernetes 和 WebAssembly 相结合可以进一步提升应用的可移植性。 微软的 Deis Labs 年初发布了一个 实验项目 ,来利用 Virtual Kubelet 类似的架构调度 WebAssembly 应用。但是这个方式有很多局限,无法借助容器方式进行应用分发,也无法利用 K8s 的语义进行资源编排。  难得有一个春节假期可以宅在家里,在此期间我基于 Derek McGowan 去年的一个 实验性项目 ,完善了 containerd 的 WASM shim 实现。可以让 containerd 支持 WASM container,并且可以利用 Kubernetes 集群管理和调度 WASM container。 项目的代码实现: https://github.com/denverdino/containerd-wasm 注:这个项目更多是概念验证,进程管理、资源限制,性能优化等的细节并没未完整实现。 整个系统的架构设计如下,“container-shim-wasm-v1”作为 Containerd 的扩展,利用 wasmer 作为 WASM 应用运行时环境,可以实现与 runc 容器一致的用户体验。 我们还会将其注册为 K8s 的一个  RuntimeClass ,允许用户利用 K8s 来交付和运维 WASM 应用。 注:RuntimeClass 是 Kubernetes v1.12 引入的新概念,可以让 Kubernetes 支持多种不同的容器运行时,比如 runc 容器、或者 Kata Containers,gVisor 等安全沙箱容器。更多细节可以参考: containerd 与安全沙箱的 Kubernetes 初体验 。 Talk is Cheap, 放码过来 首先,我们将利用 Minikube 创建一个 K8s 测试环境,并将 Containerd 作为 Kubernetes 集群的容器运行时。 创建虚拟机测试环境 创建 Minikube K8s 集群,并将 Containerd 作为 Kubernetes 集群容器运行时。 minikube start --image-mirror-country cn \ --iso-url=https://kubernetes.oss-cn-hangzhou.aliyuncs.com/minikube/iso/minikube-v1.6.0.iso \ --registry-mirror=https://tgtsuwdg.mirror.aliyuncs.com \ --container-runtime=containerd 进入 Minikube 虚拟机: $ minikube ssh _ _ _ _ ( ) ( ) ___ ___ (_) ___ (_)| |/') _ _ | |_ __ /' _ ` _ `\| |/' _ `\| || , < ( ) ( )| '_`\ /'__`\ | ( ) ( ) || || ( ) || || |\`\ | (_) || |_) )( ___/ (_) (_) (_)(_)(_) (_)(_)(_) (_)`\___/'(_,__/'`\____) 配置环境所需依赖: wasmer 0.13; minikube 缺省安装了 container 1.2.x,需要升级 containerd 1.3.x; 我提供了一个预编译的 containerd-wasm-shim-v1,也可自己编译一个版本。 cd ~ # Install Wasmer 0.13.1 curl -L -O https://github.com/wasmerio/wasmer/releases/download/0.13.1/wasmer-linux-amd64.tar.gz gunzip wasmer-linux-amd64.tar.gz tar xvf wasmer-linux-amd64.tar sudo cp bin/* /usr/bin/ # Upgrade containerd to v1.3.2 curl -L -O https://github.com/containerd/containerd/releases/download/v1.3.2/containerd-1.3.2.linux-amd64.tar.gz gunzip containerd-1.3.2.linux-amd64.tar.gz tar xvf containerd-1.3.2.linux-amd64.tar sudo systemctl stop containerd sudo cp bin/* /usr/bin/ sudo systemctl restart containerd # Install containerd-wasm-shim wget http://kubernetes.oss-cn-hangzhou.aliyuncs.com/containerd-wasm/containerd-shim-wasm-v1 chmod +x containerd-shim-wasm-v1 sudo mv containerd-shim-wasm-v1 /usr/bin/ 配置 containerd 支持 WASM shim 在 containerd 配置文件中添加 wasm shim 相关配置,并重启 containerd。 $ cat <
来源:OSCHINA
发布时间:2020-03-11 11:09:00
随着技术的发展,各种应用变得越来越复杂,开发的压力也与日俱增,速度与质量等等各种要求更是让企业的基础架构、IT 团队及工作流程压力山大。 假设一下,你正在使用笔记本电脑开发应用,并且开发环境有特定配置,其他开发人员的环境配置可能稍有不同。而你正在开发的应用不止依赖于你当前的配置,还需要某些特定的库、依赖项和文件。另一边,你的企业还有标准化的开发和生产环境、配置和支持文件等等,你希望尽可能多在本地模拟这些环境,而不产生额外的开销。这个时候你该如何确保应用能在这些环境中运行和通过质检,并在部署过程中没有让人头疼的问题,也不需要重新写代码和故障修复? 答案就是:使用容器。 容器可以确保你的应用有用必需的库、依赖项和文件,你可以在生产中自如迁移这些应用,无需担心出现任何负面影响。 容器这个词,字面释义代表的是“可容纳物料的器具”,简单来说就是“装”。那么在 IT 世界里,容器技术就是 Linux Container 的直译。没错,自从 Linux 提出以后,容器已经发展成为云平台上不可或缺的技术。Linux 可以帮助你跨多种环境,作为云原生中最基础的计算单元,其标准化、轻量级、易移植、安全等特点正在受到越来越多的企业欢迎。 除了 Linux,在当前,Docker 更是大家都在讨论的热点,几乎是容器的代名词。但在容器世界里,要想了解 Docker,得先搞清楚: 容器的基本构成是什么?容器的典型应用是怎么样的?有哪些使用容器的优秀案例值得参考?如何将容器应用到实际业务中提升效率? 六周玩转云原生 为了让开发者们在这个特殊的时期里可以学习到更多干货,京东智联云开发者特别策划 《六周玩转云原生》系列课程 ,让你迅速入门,持续充电。 3 月 12 日,第一节针对容器方向的公开课《容器入门,Docker、Pod初探》将隆重开讲! 本次公开课邀请到京东云与 AI 云产品研发部专家架构师 刘俊辉老师,从容器的基本构成出发,深入浅出地勾勒出容器的基本情况,包括容器的结构、基本使用方式等。不仅如此,还将引出 Docker 这一重要概念,为开发者们拨开容器应用的迷雾。此外,刘俊辉还会着重讲解 POD、了解 POD 的构成和基本应用,帮助开发者对容器有更深入的理解。 容器入门,Docker、Pod初探 主讲人:刘俊辉 (京东云与AI云产品研发部专家架构师) 课程大纲 课程时间 03月12日(周四)20:00-21:00 扫描下方二维码 立即报名 注意!!报名成功后,开课前会有短信/邮件提醒,所以报名时请填写正确的手机号码及邮箱地址哦! 添加小助手,回复: 玩转云原生 进入公开课交流群 👇👇👇 欢迎点击“ 京东云 ”了解更多精彩内容!
来源:OSCHINA
发布时间:2020-03-10 23:08:00