返回顶部
首页 > 资讯 > 精选 >OpenStack容器服务Zun初探与原理分析
  • 119
分享到

OpenStack容器服务Zun初探与原理分析

2023-06-04 10:06:29 119人浏览 安东尼
摘要

01Zun服务简介Zun是OpenStack的容器服务(Containers as Service),类似于AWS的ECS服务,但实现原理不太一样,ECS是把容器启动在EC2虚拟机实例上,而Zun会把容器直接运行在compute节点上。和O

01

Zun服务简介

Zun是OpenStack的容器服务(Containers as Service),类似于AWS的ECS服务,但实现原理不太一样,ECS是把容器启动在EC2虚拟机实例上,而Zun会把容器直接运行在compute节点上。

和OpenStack另一个容器相关的Magnum项目不一样的是:Magnum提供的是容器编排服务,能够提供弹性kubernetes、Swarm、Mesos等容器基础设施服务,管理的单元是Kubernetes、Swarm、Mesos集群,而Zun提供的是原生容器服务,支持不同的runtime如Docker、Clear Container等,管理的单元是container。

Zun服务的架构如图:

OpenStack容器服务Zun初探与原理分析

Zun服务和Nova服务的功能和结构非常相似,只是前者提供容器服务,后者提供虚拟机服务,二者都是主流的计算服务交付模式。功能类似体现在如下几点:

  • 通过Neutron提供网络服务。

  • 通过Cinder实现数据的持久化存储。

  • 都支持使用Glance存储镜像。

  • 其他如quota、安全组等功能。

组件结构结构相似则表现在:

  • 二者都是由api、调度、计算三大组件模块构成,Nova由nova-api、nova-scheduler、nova-compute三大核心组件构成,而Zun由zun-api、zun-compute两大核心组件构成,之所以没有zun-scheduler是因为scheduler集成到zun-api中了。

  • nova-compute调用compute driver创建虚拟机,如Libvirt。zun-compute调用container driver创建容器,如Docker。

  • Nova通过一系列的proxy代理实现VNC(nova-novncproxy)、Splice(nova-spiceproxy)等虚拟终端访问,Zun也是通过proxy代理容器的websocket实现远程attach容器功能。

02

Zun服务部署

Zun服务部署和Nova、Cinder部署模式类似,控制节点创建数据库、Keystone创建service以及注册endpoints等,最后安装相关包以及初始化配置。计算节点除了安装zun-compute服务,还需要安装要使用的容器,比如Docker。详细的安装过程可以参考官方文档,如果仅仅是想进行POC测试,可以通过DevStack自动化快速部署一个AllInOne环境,供参考的local.conf配置文件如下:

OpenStack容器服务Zun初探与原理分析

如上配置会自动通过DevStack安装Zun相关组件、Kuryr组件以及Docker。

03

Zun服务入门

3.1 Dashboard

安装Zun服务之后,可以通过zun命令行以及Dashboard创建和管理容器。

有一个非常赞的功能是如果安装了Zun,Dashboard能够支持Cloud shell,用户能够在DashBoard中进行交互式输入OpenStack命令行。

OpenStack容器服务Zun初探与原理分析

原理的话就是通过Zun启动了一个gbraad/openstack-client:alpine容器。

通过Dashboard创建容器和创建虚拟机的过程非常相似,都是通过panel依次选择镜像(image)、选择规格(Spec)、选择或者创建卷(volume)、选择网络(network/port)、选择安全组(SecuiryGroup)以及scheduler hint,如图:

OpenStack容器服务Zun初探与原理分析

其中Miscellaneous杂项中则为针对容器的特殊配置,比如设置环境变量(Environment)、工作目录(Working Directory)等。

3.2 命令行操作

通过命令行创建容器也非常类似,使用过nova以及docker命令行的基本不会有困难,下面以创建一个mysql容器为例:

OpenStack容器服务Zun初探与原理分析
  • 如上通过--mount参数指定了volume大小,由于没有指定volume_id,因此Zun会新创建一个volume。需要注意的是,Zun创建的volume在容器删除后,volume也会自动删除(auto remove),如果需要持久化volume卷,则应该先通过Cinder创建一个volume,然后通过source选项指定volume_id,此时当容器删除时不会删除已有的volume卷。

  • 和虚拟机不一样,虚拟机通过flavor配置规格,容器则直接指定cpu、memory、disk。

  • 如上没有指定--image-driver参数,则默认从dockerhub下载镜像,如果指定glance,则会往glance下载镜像。

另外Mysql容器初始化时数据卷必须为空目录,挂载的volume新卷格式化时会自动创建lost+found目录,因此需要手动删除,否则mysql容器会初始化失败:

OpenStack容器服务Zun初探与原理分析

创建完成后可以通过zun list命令查看容器列表:

OpenStack容器服务Zun初探与原理分析

可以看到mysql的容器fixed IP为192.168.233.80,和虚拟机一样,租户IP默认与外面不通,需要绑定一个浮动IP(floating ip),

OpenStack容器服务Zun初探与原理分析

zun命令行目前还无法查看floating ip,只能通过neutron命令查看,获取到floatingip并且安全组入访允许3306端口后就可以远程连接mysql服务了:

OpenStack容器服务Zun初探与原理分析

当然在同一租户的虚拟机也可以直接通过fixed ip访问mysql服务:

OpenStack容器服务Zun初探与原理分析

可见,通过容器启动mysql服务和在虚拟机里面部署mysql服务,用户访问上没有什么区别,在同一个环境中,虚拟机和容器可共存,彼此可相互通信,在应用层上可以完全把虚拟机和容器透明化使用,底层通过应用场景选择虚拟机或者容器。

3.3 关于capsule

Zun除了管理容器container外,还引入了capsule的概念,capsule类似Kubernetes的pod,一个capsule可包含多个container,这些container共享network、ipc、pid namespace等。

通过capsule启动一个mysql服务,声明yaml文件如下:

OpenStack容器服务Zun初探与原理分析

创建mysql capsule:

OpenStack容器服务Zun初探与原理分析

可见capsule的init container用的就是kubernetes的pause镜像。

3.4 总结

OpenStack的容器服务本来是在Nova中实现的,实现了Nova ComputeDriver,因此Zun的其他的功能如容器生命周期管理、image管理、service管理、action管理等和Nova虚拟机非常类似,可以查看官方文档,这里不再赘述。

04

Zun实现原理

4.1 调用容器接口实现容器生命周期管理

前面提到过Zun主要由zun-api和zun-compute服务组成,zun-api主要负责接收用户请求、参数校验、资源准备等工作,而zun-compute则真正负责容器的管理,Nova的后端通过compute_driver配置,而Zun的后端则通过container_driver配置,目前只实现了DockerDriver。因此调用Zun创建容器,最终就是zun-compute调用docker创建容器。

下面以创建一个container为例,简述其过程。

4.1.1 zun-api

首先入口为zun-api,主要代码实现在zun/api/controllers/v1/containers.py以及zun/compute/api.py,创建容器的方法入口为post()方法,其调用过程如下:

zun/api/controllers/v1/containers.py

  1. policy enforce: 检查policy,验证用户是否具有创建container权限的API调用。

  2. check security group: 检查安全组是否存在,根据传递的名称返回安全组的ID。

  3. check container quotas: 检查quota配额。

  4. build requested network: 检查网络配置,比如port是否存在、network id是否合法,最后构建内部的network对象模型字典。注意,这一步只检查并没有创建port。

  5. create container object:根据传递的参数,构造container对象模型。

  6. build requeted volumes: 检查volume配置,如果传递的是volume id,则检查该volume是否存在,如果没有传递volume id只指定了size,则调用Cinder API创建新的volume。

zun/compute/api.py

  1. schedule container: 使用FilterScheduler调度container,返回宿主机的host对象。这个和nova-scheduler非常类似,只是Zun集成到zun-api中了。目前支持的filters如CPUFilter、RamFilter、LabelFilter、ComputeFilter、RuntimeFilter等。

  2. image validation: 检查镜像是否存在,这里会远程调用zun-compute的image_search方法,其实就是调用docker search。这里主要为了实现快速失败,避免到了compute节点才发现image不合法。

  3. record action: 和Nova的record action一样,记录container的操作日志

  4. rpc cast container_create: 远程异步调用zun-compute的container_create()方法,zun-api任务结束。

4.1.2 zun-compute

zun-compute负责container创建,代码位于zun/compute/manager.py,过程如下:

  1. wait for volumes avaiable: 等待volume创建完成,状态变为avaiable。

  2. attach volumes:挂载volumes,挂载过程后面再介绍。

  3. checksupportdisk_quota: 如果使用本地盘,检查本地的quota配额。

  4. pull or load image: 调用Docker拉取或者加载镜像。

  5. 创建docker network、创建neutron port,这个步骤下面详细介绍。

  6. create container: 调用Docker创建容器。

  7. container start: 调用Docker启动容器。

以上调用Dokcer拉取镜像、创建容器、启动容器的代码位于zun/container/docker/driver.py,该模块基本就是对社区Docker SDK for python的封装。

OpenStack容器服务Zun初探与原理分析

Zun的其他操作比如start、stop、kill等实现原理也类似,这里不再赘述。

4.2 通过websocket实现远程容器访问

我们知道虚拟机可以通过VNC远程登录,物理服务器可以通过SOL(IPMI Serial Over LAN)实现远程访问,容器则可以通过WEBSocket接口实现远程交互访问。

Docker原生支持websocket连接,参考APIAttach to a container via a websocket,websocket地址为/containers/{id}/attach/ws,不过只能在计算节点访问,那如何通过API访问呢?

和Nova、Ironic实现完全一样,也是通过proxy代理转发实现的,负责container的websocket转发的进程为zun-wsproxy。

当调用zun-compute的container_attach()方法时,zun-compute会把container的websocket_url以及websocket_token保存到数据库中.

OpenStack容器服务Zun初探与原理分析

zun-wsproxy则可读取container的websocket_url作为目标端进行转发:

OpenStack容器服务Zun初探与原理分析

通过Dashboard可以远程访问container的shell:

OpenStack容器服务Zun初探与原理分析

当然通过命令行zun attach也可以attach container。

4.3 使用Cinder实现容器持久化存储

前面介绍过Zun通过Cinder实现container的持久化存储,之前我的另一篇文章介绍了Docker使用OpenStack Cinder持久化volume原理分析及实践,介绍了john griffith开发的docker-cinder-driver以及OpenStack Fuxi项目,这两个项目都实现了Cinder volume挂载到Docker容器中。另外cinderclient的扩展模块Python-brick-cinderclient-ext实现了Cinder volume的local attach,即把Cinder volume挂载到物理机中。

Zun没有复用以上的代码模块,而是重新实现了volume attach的功能,不过实现原理和上面的方法完全一样,主要包含如下过程:

  1. connect volume: connect volume就是把volume attach(映射)到container所在的宿主机上,建立连接的的协议通过initialize_connection信息获取,如果是LVM类型则一般通过iscsi,如果是Ceph rbd则直接使用rbd map。

  2. ensure mountpoit tree: 检查挂载点路径是否存在,如果不存在则调用mkdir创建目录。

  3. make filesystem:如果是新的volume,挂载时由于没有文件系统因此会失败,此时会创建文件系统。

  4. do mount: 一切准备就绪,调用OS的mount接口挂载volume到指定的目录点上。

Cinder Driver的代码位于`zun/volume/driver.py的Cinder类中,方法如下:

OpenStack容器服务Zun初探与原理分析

其中cinder.attach_volume()实现如上的第1步,而_mount_device()实现了如上的2-4步。

4.4 集成Neutron网络实现容器网络多租户

4.4.1 关于容器网络

前面我们通过Zun创建容器,使用的就是Neutron网络,意味着容器和虚拟机完全等同的共享Neutron网络服务,虚拟机网络具有的功能,容器也能实现,比如多租户隔离、floating ip、安全组、防火墙等。

Docker如何与Neutron网络集成呢?根据官方Docker network plugin API介绍,插件位于如下目录:

  • /run/docker/plugins

  • /etc/docker/plugins

  • /usr/lib/docker/plugins

OpenStack容器服务Zun初探与原理分析

由此可见Docker使用的是kuryr网络插件。

Kuryr也是OpenStack中一个较新的项目,其目标是“Bridge between container framework networking and storage models to OpenStack networking and storage abstractions.”,即实现容器与OpenStack的网络与存储集成,当然目前只实现了网络部分的集成。

而我们知道目前容器网络主要有两个主流实现模型:

  • CNM:Docker公司提出,Docker原生使用的该方案,通过Http请求调用,模型设计可参考The Container Network Model Design,network插件可实现两个Driver,其中一个为IPAM Driver,用于实现IP地址管理,另一个为Docker Remote Drivers,实现网络相关的配置。

  • CNI:CoreOS公司提出,Kubernetes选择了该方案,通过本地方法或者命令行调用。

因此Kuryr也分成两个子项目,kuryr-network实现CNM接口,主要为支持原生的Docker,而kury-kubernetes则实现的是CNI接口,主要为支持Kubernetes,Kubernetes service还集成了Neutron LBaaS,下次再单独介绍这个项目。

由于Zun使用的是原生的Docker,因此使用的是kuryr-network项目,实现的是CNM接口,通过remote driver的形式注册到Docker libnetwork中,Docker会自动向插件指定的socket地址发送HTTP请求进行网络操作,我们的环境是http://127.0.0.1:23750,即kuryr-libnetwork.service监听的地址,Remote API接口可以参考Docker Remote Drivers。

4.4.2 kuryr实现原理

前面4.1节介绍到zun-compute会调用docker driver的create()方法创建容器,其实这个方法不仅仅是调用python docker sdk的create_container()方法,还做了很多工作,其中就包括网络相关的配置。

首先检查Docker的network是否存在,不存在就创建,network name为Neutron network的UUID,

OpenStack容器服务Zun初探与原理分析

然后会调用Neutron创建port,从这里可以得出结论,容器的port不是Docker libnetwork也不是Kuryr创建的,而是Zun创建的。

回到前面的Remote Driver,Docker libnetwork会首先POST调用kuryr的/IpamDriver.RequestAddressAPI请求分配IP,但显然前面Zun已经创建好了port,port已经分配好了IP,因此这个方法其实就是走走过场。如果直接调用docker命令指定kuryr网络创建容器,则会调用该方法从Neutron中创建一个port。

接下来会POST调用kuryr的/NetworkDriver.CreateEndpoint方法,这个方法最重要的步骤就是binding,即把port attach到宿主机中,binding操作单独分离出来为kuryr.lib库,这里我们使用的是veth driver,因此由kuryr/lib/binding/drivers/veth.py模块的port_bind()方法实现,该方法创建一个veth对,其中一个为tap-xxxx,xxxx为port ID前缀,放在宿主机的namespace,另一个为t_cxxxx放到容器的namespace,t_cxxxx会配置上IP,而tap-xxxx则调用shell脚本(脚本位于/usr/local/libexec/kuryr/)把tap设备添加到ovs br-int桥上,如果使用HYBRID_PLUG,即安全组通过linux Bridge实现而不是OVS,则会创建qbr-xxx,并创建一个veth对关联到ovs br-int上。

从这里可以看出,Neutron port绑定到虚拟机和容器基本没有什么区别,如下所示:

OpenStack容器服务Zun初探与原理分析

唯一不同的就是虚拟机是把tap设备直接映射到虚拟机的虚拟设备中,而容器则通过veth对,把另一个tap放到容器的namespace中。

有人会说,br-int的流表在哪里更新了?这其实是和虚拟机是完全一样的,当调用port update操作时,neutron server会发送RPC到L2 agent中(如neutron-openvswitch-agent),agent会根据port的状态更新对应的tap设备以及流表。

因此其实kuryr只干了一件事,那就是把Zun申请的port绑定到容器中。

05

总结

OpenStack Zun项目非常完美地实现了容器与Neutron、Cinder的集成,加上Ironic裸机服务,OpenStack实现了容器、虚拟机、裸机共享网络与存储。未来我觉得很长一段时间内裸机、虚拟机和容器将在数据中心混合存在,OpenStack实现了容器和虚拟机、裸机的完全平等、资源共享以及功能对齐,应用可以根据自己的需求选择容器、虚拟机或者裸机,使用上没有什么区别,用户只需要关心业务针对性能的需求以及对硬件的特殊访问,对负载(workload)是完全透明的。

参考文献

  • docker python sdk: https://docker-py.readthedocs.io/en/stable/

  • Zun’s documentation: https://docs.openstack.org/zun/latest/

  • https://docs.docker.com/engine/api/v1.39/#operation/ContainerAttachWebsocket

  • http://int32bit.me/2017/10/04/Docker使用OpenStack-Cinder持久化volume原理分析及实践/

  • https://specs.openstack.org/openstack/cinder-specs/specs/mitaka/use-cinder-without-nova.html

  • https://docs.docker.com/engine/extend/plugin_api/

  • https://GitHub.com/docker/libnetwork/blob/master/docs/design.md

  • https://github.com/docker/libnetwork/blob/master/docs/ipam.md

  • https://github.com/docker/libnetwork/blob/master/docs/remote.md

  • https://docs.openstack.org/kuryr-libnetwork/latest/

  • https://docs.openstack.org/magnum/latest/user/

  • https://github.com/docker/libnetwork

  • https://www.nuagenetworks.net/blog/container-networking-standards/

  • http://blog.kubernetes.io/2016/01/why-Kubernetes-doesnt-use-libnetwork.html

--结束END--

本文标题: OpenStack容器服务Zun初探与原理分析

本文链接: https://lsjlt.com/news/237490.html(转载时请注明来源链接)

有问题或投稿请发送至: 邮箱/279061341@qq.com    QQ/279061341

猜你喜欢
  • OpenStack容器服务Zun初探与原理分析
    01Zun服务简介Zun是OpenStack的容器服务(Containers as Service),类似于AWS的ECS服务,但实现原理不太一样,ECS是把容器启动在EC2虚拟机实例上,而Zun会把容器直接运行在compute节点上。和O...
    99+
    2023-06-04
  • docker容器的原理分析
    目录01 容器的本质是什么?02 Cgroup技术和Namespace技术介绍03 容器、镜像和仓库之间的关系01 容器的本质是什么?    ...
    99+
    2024-04-02
  • 云服务器初始化失败原因分析
    服务器配置不正确 云服务器是一种虚拟的服务器,它的配置文件通常存储在云服务器提供商的服务器上。服务器配置错误是导致云服务器初始化失败的一个常见原因。例如,服务器配置文件中的配置可能包括网络设置,服务器硬件规格和操作系统类型等。在云服务...
    99+
    2023-10-27
    初始化 原因 服务器
  • 解析探秘fescar分布式事务实现原理
    目录前言项目说明fescar的TXC模型项目结构解析通过【examples】模块的实例看下效果第一步、第二步、第三步、fescar事务过程分析首先分析配置文件【TM】模块启动全局事务...
    99+
    2024-04-02
  • Go语言编译器原理解析与应用探讨
    Go语言编译器原理解析与应用探讨 一、Go语言编译器的基本原理 Go语言是一种开发人员使用的高效、可靠且简单的编程语言,同时也具有并行性和并发性。Go语言的编译器是将Go语言代码转换为...
    99+
    2024-03-11
    应用 编译器 go语言
  • mysql触发器原理与用法实例分析
    本文实例讲述了mysql触发器原理与用法。分享给大家供大家参考,具体如下: 本文内容: 什么是触发器 创建触发器 单条触发器语句 多条触发器语句 查看触发器 删除触...
    99+
    2022-05-22
    mysql 触发器
  • 阿里云服务器代理失败原因分析
    服务器稳定性不佳。服务器稳定性是服务器代理成功的重要保障,如果服务器稳定性不好,会导致客户的业务无法正常开展。 技术支持不到位。阿里云服务器代理需要有专业的技术团队支持,如果技术支持团队不专业,无法及时解决客户的问题,会导致客户失去对服务...
    99+
    2023-10-28
    阿里 原因 服务器
  • vue scoped与深度选择器deep的原理分析
    目录scoped的作用deep作用总结JS引入模块化概念后,变得更易于开发维护,但是css样式由于其特殊性,一直没有实现模块化,scoped的出现就是为了实现样式模块化,其本质利用属...
    99+
    2022-11-13
    vue deep 原理 vue scoped的原理 深度选择器deep vue scoped
  • Vue3源码分析调度器与watch用法原理
    目录本文主要内容调度器1.添加任务(queueJobs)2.二分法找到插入位置(findInsertionIndex)3.将执行任务的函数推入微任务队列(queueFlush)4.执...
    99+
    2023-01-28
    Vue3调度器watch Vue3 watch
  • 微服务治理与统计分析
    转载本文需注明出处:微信公众号EAWorld,违者必究。引言:微服务架构下,服务拆得越细,服务的粒度越小,可组装性就越好;与之相对的服务之间的调用关系就会变复杂,为了保证服务更好的运行,需要对这些服务进行监控和管理。本文大家介绍下EOS微服...
    99+
    2023-06-05
  • Struts2拦截器Interceptor原理与配置的示例分析
    这篇文章将为大家详细讲解有关Struts2拦截器Interceptor原理与配置的示例分析,小编觉得挺实用的,因此分享给大家做个参考,希望大家阅读完这篇文章后可以有所收获。一、Struts2拦截器原理:Struts2拦截器的实现原理相对简单...
    99+
    2023-05-30
    struts2
  • mysql服务器查询慢的原因分析与解决方法
    本篇内容主要讲解“mysql服务器查询慢的原因分析与解决方法”,感兴趣的朋友不妨来看看。本文介绍的方法操作简单快捷,实用性强。下面就让小编来带大家学习“mysql服务器查询慢的原因分析与解决方法”吧!在开发...
    99+
    2024-04-02
  • Nacos服务注册客户端服务端原理分析
    目录正文版本 2.1.1客户端NacosAutoServiceRegistrationNacosServiceRegistry服务端心跳包实例注册创建 Service总结正文 在分...
    99+
    2023-02-09
    Nacos服务注册原理 Nacos服务注册
  • 腾讯云服务器停服原因分析
    经过分析,这次故障的原因主要包括以下几个方面: 首先,是腾讯云服务器的硬件设备出现故障。在服务器硬件故障之前,腾讯云服务器已经经过多次升级和维护,但在这次突发故障中,硬件设备出现了短暂的故障,导致服务器无法正常运转。 其次,是腾讯云服务器...
    99+
    2023-10-27
    腾讯 原因 服务器
  • 服务器连接阿里云服务器失败原因分析与解决方法
    在当今的数字化时代,服务器已经成为企业和个人的重要基础设施。阿里云服务器作为国内知名的云服务商,深受广大用户欢迎。然而,在实际使用中,服务器连接阿里云服务器失败的情况也不少见。那么,究竟是什么原因导致了这种问题的出现呢?本文将为大家详细分析...
    99+
    2023-11-20
    服务器 阿里 解决方法
  • 阿里云服务器启动服务器异常原因分析与解决办法
    阿里云服务器是一种云上虚拟服务器,为用户提供高效稳定的应用运行环境。然而,有时在启动服务器时,可能会遇到一些异常情况,如“阿里云服务器启动服务器异常”。本文将对这个问题进行详细的分析,并给出相应的解决办法。 一、问题分析服务器配置问题:服务...
    99+
    2023-11-05
    服务器 阿里 解决办法
  • 阿里云服务器总卡住原因分析与解决方法
    本文将针对“我的阿里云服务器总卡住”这一问题进行详细分析,找出问题所在,并提供相应的解决方法。阿里云服务器作为一款高性能、高可用的云计算产品,是许多企业和个人开发者的重要工具。但是,在使用过程中,可能会遇到各种问题,如服务器卡住。这不仅会影...
    99+
    2023-10-30
    阿里 解决方法 原因
  • 阿里云上服务器很卡:原因分析与解决方案
    简介 阿里云作为国内领先的云计算服务提供商,其服务器性能一直备受用户关注。然而,有些用户反映在使用阿里云服务器时遇到了卡顿的问题。本文将对阿里云服务器卡顿的原因进行分析,并提供相应的解决方案。1. 原因分析1.1 资源不足阿里云服务器的性能...
    99+
    2024-01-31
    阿里 解决方案 原因
  • Laravel服务容器绑定与解析的示例
    这篇文章将为大家详细讲解有关Laravel服务容器绑定与解析的示例,小编觉得挺实用的,因此分享给大家做个参考,希望大家阅读完这篇文章后可以有所收获。关于服务容器  手册上是这样介绍的:Laravel 服务容器是用于管理类的依赖和执行依赖注入...
    99+
    2023-06-14
  • 云服务器如何做账务清理工作内容分析报告
    首先,需要先对企业的云服务器账务清理工作进行分析,了解企业在使用云服务器的整个过程中可能会产生的账务清理任务。 针对每个工作任务,列出清理的目标、步骤以及可能的结果,以及需要的人员、设备、软件等相关资源。 在清理的过程中,要注意账务清...
    99+
    2023-10-27
    账务 分析报告 如何做
软考高级职称资格查询
编程网,编程工程师的家园,是目前国内优秀的开源技术社区之一,形成了由开源软件库、代码分享、资讯、协作翻译、讨论区和博客等几大频道内容,为IT开发者提供了一个发现、使用、并交流开源技术的平台。
  • 官方手机版

  • 微信公众号

  • 商务合作