优云数智

优云数智 查看完整档案

填写现居城市  |  填写毕业院校  |  填写所在公司/组织填写个人主网站
编辑

优云数智(上海优铭云计算有限公司)是一家专注于提供企业级私有云产品与解决方案的云计算厂商,提供PAAS+IAAS的一站式解决方案,团队核心成员来自Google、华为、Mirantis、盛大云等一流云计算公司,公司总部位于上海,在北京、深圳设有分公司。

个人动态

优云数智 发布了文章 · 2018-09-11

技术分享 | 软件接口测试工具篇

软件接口测试工具篇

  • Python requests -

      在软件测试领域中,如果按照软件研发周期维度给测试分类的话,集成测试是一种重要的测试手段,它在单元测试和系统测试之间能够起到桥梁的作用,而接口测试又恰恰是集成测试能够抽象而且可执行的一个分支,接口测试是验证系统组件间的接口耦合交互,检测外部系统与系统之间以及内部各个子系统之间的交互点。测试的重点是要检查数据间的交换,传递和控制管理过 程,以及系统间的相互逻辑依关系等CBA时代软件的复杂度更是呈几何级增长和聚合,由此给BUG的滋生提供了肥沃的土壤,这也是接口测试的意义所在。
    

关于软件测试更多理论知识我们在另一个篇幅中分享介绍,今天主要和大家分享Python语言中requests库在接口测试中的使用。

       


  接口测试工具众多,比如大名鼎鼎的SoapUI,Apache旗下性能与接口集一身的Jmeter、Chrome浏览器Postman插件等等,都可以完成接口类型测试,各有优点,孰轻孰重可以根据项目需求和软件集成接口协议来适配选择。本篇主要给大家推荐的是基于Python语言的requests库,requests采用 Apache2 Licensed 开源协议的 HTTP 库,requests 不仅简洁易用,而且维护文档详实,社区版本支持力度活跃,用Python做接口测试requests是推荐的选择,同时requests可以和python 下的单元测试框架unittest集成,完美实现接口测试自动化,测试结束后,通过HTMLTestRunner生成测试报告,smtplib邮件发送结果。
        

下面以优云数智PaaS产品线Solar组件的接口自动化测试框架SRAT为例分享requests的使用:

一、测试环境的准备如下:

      软件
                  版本

操作系统
Windows7
Eclipse
Oxygen.1a Release (4.7.1a)
Pydev
6.4.0
Python
3.6.3
requests
2.18.4
unittest
2.1
HTMLTestRunner
0.8.2
备注:
1、Python目前分两个版本Python2x和Python3x,requests完全支持Python3x。
2、requests、unittest安装通过pip3 install xxx 安装即可。
3、HTMLTestRunner直接放到Python的Lib目录下就可以了。

二、SRAT接口自动化

Public.py 将Solar组件的每个大功能封装成为一个类,同时在类下面每一个接口定义为一个方法,然后通过requests实现接口协议封装的好的部分,在Case*.py测试用例时直接使用该公共类抽象出的对象就可以了,这样做到全局复用。
Case *.py 是将每个大类下面的具体接口编写测试用例,在测试用例里面来实现每一个测试接口所要的测试内容,每一条测试Case最后用到unitest来断言测试结果和预期结果,作为在测试报告中标注测试是否通过。
Report.py是执行自动化测试的入口,里面HTMLTestRunner定义了测试报告生成、smtplib邮件发送两大块内容。
report文件夹用来存放每次执行接口自动化测试生成的报告。
config.ini配置文件可以用来配置邮件的相关信息,例如发送接收人,邮件服务器信息等。

三、SRAT接口自动化测试公共类Public.py实现(以User API为例):

   User业务功能中总共包括8个接口,分别实现不同的和用户相关功能,将User封装为类Class UserAPI,然后在UserAPI Class中分别对每一个接口功能定义一个方法实例,准备给对应的接口测试用例来调用,这样比较逻辑清晰,易于修改和复用,下面是实现代码。 



四、SRAT接口自动化测试用例类Case*.py实现(以CaseUser 为例):

   在User API里面定义好公共类后,在CaseUser就需要使用公共类定义好的方法了,CaseUser里面继承了Python 单元测试框架 unittest的方法(关于unittest实现原理类似于java里面的junit,大家可以自行查找资料,比较好理解),测试结果和预期结果通过断言assert来比较,SRAT主要是判断接口请求后response返回的状态码和返回状态信息来断言。然后将所有的测试用例组织到测试套件unittest.TestSuite()自动化完成测试用例的执行,执行完所有的测试用例后用HTMLTestRunner.HTMLTestRunner()生成测试报告,代码如下。



五、SRAT接口自动化Report测试执行及报告发送:

   测试公共类Public和测试用例Case* 编写完成后,就可以进入测试执行环节,分两部分:一部分是测试执行环节,unittest.defaultTestLoader.discover()用来执行测试用例部分,自动调用Case*.py,Case*.py继续再调用Public对应实现方法,测试用例执行完毕后,调用send_mail()方法发送测试测试报告,整个接口测试过程就完毕了,实现代码如下。

测试执行完毕后在Eclipse 控制台显示的测试。

六、SRAT接口自动化邮件报告查看:

  下面是SRAT接口自动化测试完成HTMLTestRunner生成报告后发送给相关人员,打开可以查看本次接口测试执行情况,包括测试用例的通过率、通过、错误、失败、所有,对每一个用例集下的测试用例有详细的统计,失败或错误了的原因,如下测试报告展示。



 总结上面SRAT实现接口自动化测试分享了Python requests使用,从测试角度讲,关键部分是测试框架搭建和测试用例的编写,本次SRAT是首先将接口公共部分封装类后,然后再在测试用例调用实现公共的方法去测试对应接口,其实,这个测试还需要有一定的Python代码基础,如果从黑盒子角度可以将测试框架和测试用例完全解耦分离,测试用例用excel或YAML文件来实现测试用例就更好了,期待后面改进一版。

上面有不妥之前敬请和我联系,一起交流沟通,谢谢!后面有机会和大家一起分享测试基础理论、测试工具集合、存储测试入门、大数据性能测试等内容。

requests官方参考文档:
http://docs.python-requests.o...
http://www.python-requests.or...
https://pypi.org/project/requ...

优云数智介绍

优云数智(上海优铭云计算有限公司)是一家专注于提供企业级私有云产品与解决方案的云计算厂商,提供PaaS+IaaS的一站式解决方案。优云数智的母公司是中国中立的公有云服务商UCloud。私有云技术来源于全球顶尖的OpenStack、Ceph、Kubernetes云计算开发团队。

扫描关注,了解更多

查看原文

赞 0 收藏 0 评论 0

优云数智 发布了文章 · 2018-09-07

技术分享 | OpenShift网络之SDN

在红帽主导的容器平台OpenShift中,默认使用了原生的SDN网络解决方案,这是专门为OpenShift开发的一套符合CNI标准的SDN Plugin,并采用了当下流行的OVS作为虚拟交换机。

Overview

一个基本的SDN系统一般包含管理面、控制面和数据(转发)面三部分,通俗来说,管理面的对外表现就是北向接口,拿Openshift中的SDN来说,它的北向接口就是CNI了,kubelet收到用户创建POD的请求后,会调用CRI接口去实现POD创建相关的工作,另一块就是调用CNI去完成网络相关的工作,当然这里的CNI主要分成两块:

一部分是二进制文件,也就是kubelet直接执行该二进制文件并向其传递pod id、namespace等参数;
另一部分就是CNI Server,接收CNI Client的请求,并调用SDN Controller去完成OVS Bridge端口、流表相关的配置,为POD打通网络。

控制面一个主要载体就是南向接口了,当然管理面的一些具体实现也有可能通过南向接口实现(这里不做重点分析),对于一个DC领域SDN控制器而言,其南向协议占了很大一部分的,说道南向协议很多人可能想到是Openflow、netconf、XMPP、P4 runtime……,不过可能要让大家失望了,这里并没采用上面说的那些高大上的协议,有的只是CLI、ovs-vsctl、ovs-ofctl、iptables等我们以前常用的命令而已,通过对这些常用命令的函数化封装、调用就组成了今天要用到的SDN南向协议。

可能很多人要在心里面犯嘀咕了,仅仅通过几条命令的调用能不能胜任这份重任呢?答案当然是肯定的,没有一点问题,大名鼎鼎的OpenStack中的Neutron也是这么干的,Neutron中的默认方式也是直接调用的命令来实现对OVS的控制,所以放心用吧。

我们这里重点介绍一下该SDN方案的数据面实现模型,当SDN Controller收到北向CNI Server的网络请求后,是如何控制OVS进行流表的增删改查、以及iptables相关的操作。

数据面就是指各种转发设备了,包括传统的硬件厂家个各种路由器、交换机、防火墙、负载均衡等,也包括各种纯软件实现的是Vswitch、Vrouter,其中OVS作为其中的典型代表,借着OpenStack等云计算技术的发展着实火了一把,在Openshift的SDN方案中OVS作为数据面的转发载体,起着至关重要的作用。

北向接口

在一个基本的SDN系统中北向接口作为直接面向用户或者应用部分,一个主要的功能就是先理解用户的“语言”,这里的语言就是是CNI了,CNI也是容器平台一个重要的网络接口,基本动作包含:添加网络、删除网络、添加网络列表、删除网络列表。

在Openshift的SDN中,首先实现了一个CNI的client,编译后会产生一个二进制可执行文件,安装过程中一般会将该文件放置在/opt/cni/bin目录下,供kubelet调用。
在pkg/network/sdn-cni-plugin/openshift-sdn.go#line57文件中

// Send a CNI request to the CNI server via JSON + HTTP over a root-owned unix socket,
// and return the result
func (p cniPlugin) doCNI(url string, req cniserver.CNIRequest) ([]byte, error) {

data, err := json.Marshal(req)
if err != nil {
    returnnil, fmt.Errorf("failed to marshal  CNI request %v: %v", req, err)
}

client := &http.Client{
    Transport: &http.Transport{
        Dial: func(proto, addr string) (net.Conn, error) {
            return net.Dial("unix", p.socketPath)
        },
    },
}

varresp *http.Response
err = p.hostNS.Do(func(ns.NetNS) error {
    resp, err = client.Post(url, "application/json", bytes.NewReader(data))
    return err
})
if err != nil {
    returnnil, fmt.Errorf("failed to send CNI  request: %v", err)
}
defer resp.Body.Close()

body, err := ioutil.ReadAll(resp.Body)
if err != nil {
    returnnil, fmt.Errorf("failed to read CNI  result: %v", err)
}

if resp.StatusCode != 200 {
    returnnil, fmt.Errorf("CNI request failed  with status %v: '%s'", resp.StatusCode, string(body))
}

return body, nil

}

// Send the ADD command environment and config to the CNI server, returning
// the IPAM result to the caller
func (p cniPlugin) doCNIServerAdd(req cniserver.CNIRequest, hostVeth string) (types.Result, error) {

req.HostVeth = hostVeth
body, err := p.doCNI("http://dummy/", req)
if err != nil {
    returnnil, err
}

// We currently expect CNI version 0.2.0  results, because that's the
// CNIVersion we pass in our config JSON
result, err := types020.NewResult(body)
if err != nil {
    returnnil, fmt.Errorf("failed to  unmarshal response '%s': %v", string(body), err)
}

return result, nil

}
....

func (p cniPlugin) CmdDel(args skel.CmdArgs) error {

_, err := p.doCNI("http://dummy/", newCNIRequest(args))
return err

}

主要实现了CmdAdd、CmdDel两种类型的方法供kubelet调用,以完成POD创建或者删除时相关网络配置的变更。

其次就是实现了一个CNI的Server用来响应处理CNI客户端的请求,Server 跟Client之间通过一个unix类型的Socket以HTTP + JSON 的方式进行通信。
在Openshift中每个NODE启动时都会顺带启动一个CNI Server,在文件origin/pkg/network/node/pod.go#line170中

// Start the CNI server and start processing requests from it
func (m *podManager) Start(rundir string, localSubnetCIDR string, clusterNetworks []common.ClusterNetwork, serviceNetworkCIDR string) error {

if m.enableHostports {
    iptInterface := utiliptables.New(utilexec.New(), utildbus.New(),  utiliptables.ProtocolIpv4)
    m.hostportSyncer = kubehostport.NewHostportSyncer(iptInterface)
}

varerrerror
ifm.ipamConfig, err = getIPAMConfig(clusterNetworks, localSubnetCIDR); err != nil {
    return err
}

go m.processCNIRequests()

m.cniServer = cniserver.NewCNIServer(rundir,  &cniserver.Config{MTU: m.mtu, ServiceNetworkCIDR: serviceNetworkCIDR})
return m.cniServer.Start(m.handleCNIRequest)

}

其中的line184的cniserver.NewCNIServer具体实现在文件origin/pkg/network/node/cniserver/cniserver.go#line120中

// Create and return a new CNIServer object which will listen on a socket in the given path
funcNewCNIServer(rundir string, config Config) CNIServer {

router := mux.NewRouter()

s := &CNIServer{
    Server: http.Server{
        Handler: router,
    },
    rundir: rundir,
    config: config,
}
router.NotFoundHandler = http.HandlerFunc(http.NotFound)
router.HandleFunc("/", s.handleCNIRequest).Methods("POST")
return s

}

…….
// Start the CNIServer's local HTTP server on a root-owned Unix domain socket.
// requestFunc will be called to handle pod setup/teardown operations on each
// request to the CNIServer's HTTP server, and should return a PodResult
// when the operation has completed.
func (s *CNIServer) Start(requestFunc cniRequestFunc) error {

if requestFunc == nil {
    return fmt.Errorf("no pod request  handler")
}
s.requestFunc = requestFunc

CNIServer收到Client的请求后会继续调用后端的ovscontroller等文件完成具体的网络实现。

南向接口

就像上面所说的,在Openshift的SDN中,南向协议是直接通过调用CLI命令来实现的,下面我们简单看一下具体实现

origin/pkg/util/ovs/ovs.go#line140

....
const (

OVS_OFCTL = "ovs-ofctl"
OVS_VSCTL = "ovs-vsctl"

)
....

func (ovsif *ovsExec) execWithStdin(cmd string, stdinArgs []string, args ...string) (string, error) {

logLevel := glog.Level(4)
switch cmd {
case OVS_OFCTL:
    if args[0] == "dump-flows" {
        logLevel = glog.Level(5)
    }
    args = append([]string{"-O", "OpenFlow13"}, args...)
case OVS_VSCTL:
    args = append([]string{"--timeout=30"}, args...)
}

kcmd := ovsif.execer.Command(cmd, args...)
if stdinArgs != nil {
    stdinString := strings.Join(stdinArgs, "\n")
    stdin := bytes.NewBufferString(stdinString)
    kcmd.SetStdin(stdin)

    glog.V(logLevel).Infof("Executing: %s %s  <<\n%s", cmd, strings.Join(args, " "), stdinString)
} else {
    glog.V(logLevel).Infof("Executing: %s  %s",  cmd, strings.Join(args, " "))
}

output, err := kcmd.CombinedOutput()
if err != nil {
    glog.V(2).Infof("Error executing  %s: %s",  cmd, string(output))
    return"", err
}

outStr := string(output)
if outStr != "" {
    // If output is a single  line, strip the trailing newline
    nl := strings.Index(outStr, "\n")
    if nl == len(outStr)-1 {
        outStr = outStr[:nl]
    }
}
return outStr, nil

}

这里主要对OVS中两个常用的命令ovs-vsctl、ovs-ifctl进行了函数化封装,后续的增加ovs端口、转发流表、QOS限速等,均可以通过调用该函数来实现对OVS配置,

origin/pkg/network/node/iptables.go#line90

// syncIPTableRules syncs the cluster network cidr iptables rules.
// Called from SyncLoop() or firewalld reload()
func (n *NodeIPTables) syncIPTableRules() error {

n.mu.Lock()
defer n.mu.Unlock()

start := time.Now()
deferfunc() {
    glog.V(4).Infof("syncIPTableRules  took %v", time.Since(start))
}()
glog.V(3).Infof("Syncing openshift  iptables rules")

chains := n.getNodeIPTablesChains()
fori := len(chains) - 1; i >= 0; i-- {
    chain := chains[i]
    // Create chain if it  does not already exist
    chainExisted, err := n.ipt.EnsureChain(iptables.Table(chain.table), iptables.Chain(chain.name))
    if err != nil {
        return fmt.Errorf("failed to ensure  chain %s exists: %v", chain.name, err)
    }
    if chain.srcChain != "" {
        // Create the rule  pointing to it from its parent chain. Note that since we
        // use iptables.Prepend  each time, but process the chains in reverse order,
        // chains with the same  table and srcChain (ie, OPENSHIFT-FIREWALL-FORWARD
        // and  OPENSHIFT-ADMIN-OUTPUT-RULES) will run in the same order as they
        // appear in  getNodeIPTablesChains().
        _, err = n.ipt.EnsureRule(iptables.Prepend,  iptables.Table(chain.table), iptables.Chain(chain.srcChain), append(chain.srcRule, "-j", chain.name)...)
        if err != nil {
            return fmt.Errorf("failed to ensure  rule from %s to %s exists: %v", chain.srcChain, chain.name, err)
        }
    }

    // Add/sync the rules
    rulesExisted, err := n.addChainRules(chain)
    if err != nil {
        return err
    }
    if chainExisted &&  !rulesExisted {
        // Chain existed but not  with the expected rules; this probably means
        // it contained rules  referring to a *different* subnet; flush them
        // and try again.
        iferr = n.ipt.FlushChain(iptables.Table(chain.table), iptables.Chain(chain.name)); err != nil {
            return fmt.Errorf("failed to flush  chain %s: %v", chain.name, err)
        }
        if_, err = n.addChainRules(chain); err != nil {
            return err
        }
    }
}

returnnil

}

这里主要是对iptables相关命令的封装,会在main函数中继续循环监听,同步SDN控制器下发的iptables rules并验证是否配置成功,当用户通过API创建一个Service服务时,ClusterIP相关的NAT映射、访问控制、外部网络访问均是通过调用iptables来实现。

初始化

当我们新创建一个集群或者增加一个物理节点时,SDN的Controller会对节点进行一些初始化的配置,主要包括ovs、iptables两部分

OVS

OVS的初始化主要包括网桥br0、port(tun0、vxlan)的创建、初始化流表的配置等。
我们这里重点说一下流表的模型,Openshift的SDN模型中用到的流表主要包括一下几个table:

// Table 0: initial dispatch based on in_port
// Table 10: VXLAN ingress filtering; filled in by AddHostSubnetRules()
// Table 20: from OpenShift container; validate IP/MAC, assign tenant-id; filled in by setupPodFlows
// Table 21: from OpenShift container; NetworkPolicy plugin uses this for connection tracking
// Table 25: IP from OpenShift container via Service IP; reload tenant-id; filled in by setupPodFlows
// Table 30: general routing
// Table 40: ARP to local container, filled in by setupPodFlows
// Table 50: ARP to remote container; filled in by AddHostSubnetRules()
// Table 60: IP to service from pod
// Table 70: IP to local container: vnid/port mappings; filled in by setupPodFlows
// Table 80: IP policy enforcement; mostly managed by the osdnPolicy
// Table 90: IP to remote container; filled in by AddHostSubnetRules()
// Table 100: egress routing; edited by UpdateNamespaceEgressRules()
// Table 101: egress network policy dispatch; edited by UpdateEgressNetworkPolicy()
// Table 110: outbound multicast filtering, updated by UpdateLocalMulticastFlows()
// Table 111: multicast delivery from local pods to the VXLAN; only one rule, updated by UpdateVXLANMulticastRules()
// Table 120: multicast delivery to local pods (either from VXLAN or local pods); updated by UpdateLocalMulticastFlows()
// Table 253: rule version note

调度关系如下:

origin/pkg/network/node/ovscontroller.go#line66

func (oc *ovsController) SetupOVS(clusterNetworkCIDR []string, serviceNetworkCIDR, localSubnetCIDR, localSubnetGateway string, mtu uint32) error {
....

err = oc.ovs.AddBridge("fail-mode=secure", "protocols=OpenFlow13")
if err != nil {
    return err

....

_ = oc.ovs.DeletePort(Vxlan0)
_, err = oc.ovs.AddPort(Vxlan0, 1, "type=vxlan", `options:remote_ip="flow"`, `options:key="flow"`)
if err != nil {
    return err
}
_ = oc.ovs.DeletePort(Tun0)
_, err = oc.ovs.AddPort(Tun0, 2, "type=internal", fmt.Sprintf("mtu_request=%d", mtu))
if err != nil {
    return err
}

otx := oc.ovs.NewTransaction()

// Table 0: initial dispatch based on in_port
if oc.useConnTrack {
    otx.AddFlow("table=0, priority=300, ip, ct_state=-trk,  actions=ct(table=0)")
}
// vxlan0
for_, clusterCIDR := range clusterNetworkCIDR {
    otx.AddFlow("table=0, priority=200, in_port=1, arp, nw_data-original=%s,  nw_dst=%s,  actions=move:NXM_NX_TUN_ID[0..31]->NXM_NX_REG0[],goto_table:10", clusterCIDR,  localSubnetCIDR)
    otx.AddFlow("table=0, priority=200, in_port=1, ip, nw_data-original=%s,  actions=move:NXM_NX_TUN_ID[0..31]->NXM_NX_REG0[],goto_table:10", clusterCIDR)
    otx.AddFlow("table=0, priority=200, in_port=1, ip, nw_dst=%s,  actions=move:NXM_NX_TUN_ID[0..31]->NXM_NX_REG0[],goto_table:10", clusterCIDR)
}
otx.AddFlow("table=0, priority=150, in_port=1, actions=drop")
// tun0
if oc.useConnTrack {
    otx.AddFlow("table=0, priority=400, in_port=2, ip, nw_data-original=%s,  actions=goto_table:30", localSubnetGateway)
    for_, clusterCIDR := range clusterNetworkCIDR {
        otx.AddFlow("table=0, priority=300, in_port=2, ip, nw_data-original=%s,  nw_dst=%s, actions=goto_table:25", localSubnetCIDR, clusterCIDR)
    }
}
otx.AddFlow("table=0, priority=250, in_port=2, ip,  nw_dst=224.0.0.0/4, actions=drop")
for_, clusterCIDR := range clusterNetworkCIDR {
    otx.AddFlow("table=0, priority=200, in_port=2, arp, nw_data-original=%s,  nw_dst=%s, actions=goto_table:30", localSubnetGateway, clusterCIDR)
}
otx.AddFlow("table=0, priority=200, in_port=2, ip,  actions=goto_table:30")
otx.AddFlow("table=0, priority=150, in_port=2, actions=drop")
// else, from a container
otx.AddFlow("table=0, priority=100, arp, actions=goto_table:20")
otx.AddFlow("table=0, priority=100, ip, actions=goto_table:20")
otx.AddFlow("table=0, priority=0, actions=drop")

// Table 10: VXLAN ingress filtering; filled in  by AddHostSubnetRules()
// eg, "table=10, priority=100,  tun_data-original=${remote_node_ip}, actions=goto_table:30"
otx.AddFlow("table=10, priority=0, actions=drop")

// (${tenant_id} is  always 0 for single-tenant)
otx.AddFlow("table=20, priority=0, actions=drop")

// Table 21: from OpenShift container;  NetworkPolicy plugin uses this for connection tracking
otx.AddFlow("table=21, priority=0, actions=goto_table:30")

// Table 253: rule version note
otx.AddFlow("table=%d, actions=note:%s", ruleVersionTable, oc.getVersionNote())

return otx.Commit()

}

IPtables

主要涉及Cluster Network、Service Network 网段相关的rule规则、NAT以及vxlan流量(udp.port 4789)的安全策略。

origin/pkg/network/node/iptables.go#line146

func (n *NodeIPTables) getNodeIPTablesChains() []Chain {

varchainArray []Chain

chainArray = append(chainArray,
     Chain{
         table:    "filter",
         name:     "OPENSHIFT-FIREWALL-ALLOW",
         srcChain: "INPUT",
         srcRule:  []string{"-m", "comment", "--comment", "firewall  overrides"},
         rules: [][]string{
             {"-p", "udp", "--dport", vxlanPort, "-m", "comment", "--comment", "VXLAN  incoming", "-j", "ACCEPT"},
             {"-i", Tun0, "-m", "comment", "--comment", "from SDN to  localhost", "-j", "ACCEPT"},
             {"-i", "docker0", "-m", "comment", "--comment", "from docker to  localhost", "-j", "ACCEPT"},
         },
    },
     Chain{
         table:    "filter",
         name:     "OPENSHIFT-ADMIN-OUTPUT-RULES",
         srcChain: "FORWARD",
         srcRule:  []string{"-i", Tun0, "!", "-o", Tun0, "-m", "comment", "--comment", "administrator  overrides"},
         rules:    nil,
    },
)

varmasqRules [][]string
varmasq2Rules [][]string
varfilterRules [][]string
for_, cidr := range n.clusterNetworkCIDR {
    if n.masqueradeServices {
         masqRules = append(masqRules, []string{"-s", cidr, "-m", "comment", "--comment", "masquerade  pod-to-service and pod-to-external traffic", "-j", "MASQUERADE"})
    } else {
         masqRules = append(masqRules, []string{"-s", cidr, "-m", "comment", "--comment", "masquerade  pod-to-external traffic", "-j", "OPENSHIFT-MASQUERADE-2"})
         masq2Rules = append(masq2Rules, []string{"-d", cidr, "-m", "comment", "--comment", "masquerade  pod-to-external traffic", "-j", "RETURN"})
    }

    filterRules = append(filterRules, []string{"-s", cidr, "-m", "comment", "--comment", "attempted resend  after connection close", "-m", "conntrack", "--ctstate", "INVALID", "-j", "DROP"})
    filterRules = append(filterRules, []string{"-d", cidr, "-m", "comment", "--comment", "forward traffic  from SDN", "-j", "ACCEPT"})
    filterRules = append(filterRules, []string{"-s", cidr, "-m", "comment", "--comment", "forward traffic to  SDN",  "-j", "ACCEPT"})
}

chainArray = append(chainArray,
     Chain{
         table:    "nat",
         name:     "OPENSHIFT-MASQUERADE",
         srcChain: "POSTROUTING",
         srcRule:  []string{"-m", "comment", "--comment", "rules for  masquerading OpenShift traffic"},
        rules:     masqRules,
    },
     Chain{
         table:    "filter",
         name:     "OPENSHIFT-FIREWALL-FORWARD",
         srcChain: "FORWARD",
         srcRule:  []string{"-m", "comment", "--comment", "firewall  overrides"},
         rules:    filterRules,
    },
)
if !n.masqueradeServices {
    masq2Rules = append(masq2Rules, []string{"-j", "MASQUERADE"})
    chainArray = append(chainArray,
         Chain{
             table: "nat",
             name:  "OPENSHIFT-MASQUERADE-2",
             rules: masq2Rules,
         },
    )
}
return chainArray

}

举个例子
POD Add
OVS
当我们新增加一个POD时,会调用底层的CRI接口例如docker来创建POD容器,Kubelet在创建pod时是先创建一个infra容器,配置好该容器的网络,然后创建真正工作的业务容器,最后再把业务容器的网络加到infra容器的网络命名空间中,业务容器和infra容器共同组成一个pod。当kubelet创建好infra容器后,会去调用network-plugin,并将infra的namespace做为传入参数开始网络创建流程,在这里会去调用/opt/cni/bin/openshift-sdnSDN,由该二进制文件充当CNI的客户端向CNI Server发起请求,之后控制器会下发命令完成OVS新增加Port、POD IP转发相关的流标的配置。

origin/pkg/network/node/ovscontroller.go#line266

func (oc *ovsController) SetUpPod(sandboxID, hostVeth string, podIP net.IP, vnid uint32) (int, error) {

ofport, err := oc.ensureOvsPort(hostVeth, sandboxID, podIP.String())
if err != nil {
    return -1, err
}
return ofport, oc.setupPodFlows(ofport, podIP, vnid)

}
....
func (oc *ovsController) setupPodFlows(ofport int, podIP net.IP, vnid uint32) error {

otx := oc.ovs.NewTransaction()

ipstr := podIP.String()
podIP = podIP.To4()
ipmac := fmt.Sprintf("00:00:%02x:%02x:%02x:%02x/00:00:ff:ff:ff:ff", podIP[0], podIP[1], podIP[2], podIP[3])

// ARP/IP traffic from container
otx.AddFlow("table=20, priority=100, in_port=%d, arp, nw_data-original=%s,  arp_sha=%s, actions=load:%d->NXM_NX_REG0[], goto_table:21", ofport, ipstr, ipmac,  vnid)
otx.AddFlow("table=20, priority=100, in_port=%d, ip, nw_data-original=%s,  actions=load:%d->NXM_NX_REG0[], goto_table:21", ofport, ipstr, vnid)
if oc.useConnTrack {
    otx.AddFlow("table=25, priority=100, ip, nw_data-original=%s,  actions=load:%d->NXM_NX_REG0[], goto_table:30", ipstr, vnid)
}

// ARP request/response to container (not  isolated)
otx.AddFlow("table=40, priority=100, arp, nw_dst=%s,  actions=output:%d", ipstr, ofport)

// IP traffic to container
otx.AddFlow("table=70, priority=100, ip, nw_dst=%s,  actions=load:%d->NXM_NX_REG1[], load:%d->NXM_NX_REG2[],  goto_table:80", ipstr, vnid, ofport)

return otx.Commit()

}

IPtables

origin/pkg/network/node/iptables.go#line216

func (n *NodeIPTables) AddEgressIPRules(egressIP, mark string) error {

for_, cidr := range n.clusterNetworkCIDR {
    _, err := n.ipt.EnsureRule(iptables.Prepend, iptables.TableNAT, iptables.Chain("OPENSHIFT-MASQUERADE"), "-s", cidr, "-m", "mark", "--mark", mark, "-j", "SNAT", "--to-source", egressIP)
    if err != nil {
        return err
    }
}
_, err := n.ipt.EnsureRule(iptables.Append, iptables.TableFilter, iptables.Chain("OPENSHIFT-FIREWALL-ALLOW"), "-d", egressIP, "-m", "conntrack", "--ctstate", "NEW", "-j", "REJECT")
return err

}

POD delete
OVS
当一个POD别删除时,相应的SDN 控制器会调用相关删除函数,经对应的流标删除

origin/pkg/network/node/ovscontroller.go#line256

func (oc *ovsController) cleanupPodFlows(podIP net.IP) error {

ipstr := podIP.String()

otx := oc.ovs.NewTransaction()
otx.DeleteFlows("ip, nw_dst=%s", ipstr)
otx.DeleteFlows("ip, nw_data-original=%s", ipstr)
otx.DeleteFlows("arp, nw_dst=%s", ipstr)
otx.DeleteFlows("arp, nw_data-original=%s", ipstr)
return otx.Commit()

}

func (oc ovsController) DeleteServiceRules(service kapi.Service) error {

otx := oc.ovs.NewTransaction()
otx.DeleteFlows(generateBaseServiceRule(service.Spec.ClusterIP))
return otx.Commit()

}

IPtables

origin/pkg/network/node/iptables.go#line227

func (n *NodeIPTables) DeleteEgressIPRules(egressIP, mark string) error {

for_, cidr := range n.clusterNetworkCIDR {
    err := n.ipt.DeleteRule(iptables.TableNAT, iptables.Chain("OPENSHIFT-MASQUERADE"), "-s", cidr, "-m", "mark", "--mark", mark, "-j", "SNAT", "--to-source", egressIP)
    if err != nil {
        return err
    }
}
return n.ipt.DeleteRule(iptables.TableFilter, iptables.Chain("OPENSHIFT-FIREWALL-ALLOW"), "-d", egressIP, "-m", "conntrack", "--ctstate", "NEW", "-j", "REJECT")

}

Project Add

origin/pkg/network/node/vnids.go#line137

func (vmap *nodeVNIDMap) setVNID(name string, id uint32, mcEnabled bool) {

vmap.lock.Lock()
defer vmap.lock.Unlock()

ifoldId, found := vmap.ids[name]; found {
    vmap.removeNamespaceFromSet(name, oldId)
}
vmap.ids[name] = id
vmap.mcEnabled[name] = mcEnabled
vmap.addNamespaceToSet(name, id)

glog.Infof("Associate netid %d to namespace %q with mcEnabled  %v",  id, name, mcEnabled)

}

Project Delete

origin/pkg/network/node/vnids.go#line137

func (vmap *nodeVNIDMap) unsetVNID(name string) (id uint32, err error) {

vmap.lock.Lock()
defer vmap.lock.Unlock()

id, found := vmap.ids[name]
if !found {
    return0, fmt.Errorf("failed to find netid for namespace: %s in vnid map", name)
}
vmap.removeNamespaceFromSet(name, id)
delete(vmap.ids, name)
delete(vmap.mcEnabled, name)
glog.Infof("Dissociate netid %d from namespace %q", id, name)
return id, nil

}

通过上面的源码我们可以看到当进行租户项目级别的创建或者删除时,主要对VNI进行了配置调整,在Openshift的多租户网络模型中,控制器为每一个项目分配了一个VNID用来标识租户项目并实现了不同租户项目之间的隔离,当然也可以通过命令将不同项目join在一起实现两个不同项目之间的互访,底层主要通过table 80 来实现隔离或者互通。

这里只是针对基本原理以及部分源码做了一些粗浅的分析,接下来的章节会针对具体实例做分析,欢迎大家批评指正!

【推荐阅读】
《OpenFlow(OVS)下的“路由技术”》
《OpenShift源码简析之pod网络配置(上)》
《openshift源码简析之pod网络配置(下)》

优云数智介绍

优云数智(上海优铭云计算有限公司)是一家专注于提供企业级私有云产品与解决方案的云计算厂商,提供PaaS+IaaS的一站式解决方案。优云数智的母公司是中国中立的公有云服务商UCloud。私有云技术来源于全球顶尖的OpenStack、Ceph、Kubernetes云计算开发团队。

扫描关注,了解更多

查看原文

赞 0 收藏 0 评论 0

优云数智 发布了文章 · 2018-09-04

解析 | openshift源码简析之pod网络配置(下)

【编者按】openshift底层是通过kubelet来管理pod,kubelet通过CNI插件来配置pod网络.openshift node节点在启动的时会在一个goroutine中启动kubelet, 由kubelet来负责pod的管理工作。

本文主要从源码的角度入手,简单分析在openshift环境下kubelet是如何通过调用openshift sdn插件来配置pod网络。

上一节分析了openshift-sdn插件是如何配置Pod网络的,本节分析openshift-sdn插件获取Pod IP时cniServer的处理流程。
CNIServer流程

在上面的分析中我们知道,openshift-sdn插件是通过方法doCNIServerAdd向cniserver来请求IP的,那cniserver是如何处理请求的呢?我们先来看cniServer的逻辑。
cniServer的定义位于openshit代码库的pkg/network/node/cniserver/cniserver.go文件,定义如下:

1type CNIServer struct {
2 http.Server
3 requestFunc cniRequestFunc
4 rundir string
5 config *Config
6}
它包括了一个http server,以及一个处理请求的handler cniRequestFunc, 还有一些配置相关的字段。
cniSever的构造器方法位于pkg/network/node/cniserver/cniserver.go#L120, 内容如下:

1// Create and return a new CNIServer object which will listen on a socket in the given path
2func NewCNIServer(rundir string, config Config) CNIServer {
3 router := mux.NewRouter()
4
5 s := &CNIServer{
6 Server: http.Server{
7 Handler: router,
8 },
9 rundir: rundir,
10 config: config,
11 }
12 router.NotFoundHandler = http.HandlerFunc(http.NotFound)
13 router.HandleFunc("/", s.handleCNIRequest).Methods("POST")
14 return s
15}
从上面第13行的代码可以看出,该server只处理一条POST方法的路由,处理请求的handler是handleCNIRequest这个方法,该方法的定义位于 pkg/network/node/cniserver/cniserver.go#L277,内容如下:

1// Dispatch a pod request to the request handler and return the result to the
2// CNI server client
3func (s CNIServer) handleCNIRequest(w http.ResponseWriter, r http.Request) {
4 req, err := cniRequestToPodRequest(r)
5 if err != nil {
6 http.Error(w, fmt.Sprintf("%v", err), http.StatusBadRequest)
7 return
8 }
9
10 glog.V(5).Infof("Waiting for %s result for pod %s/%s", req.Command, req.PodNamespace, req.PodName)
11 result, err := s.requestFunc(req)
12 if err != nil {
13 http.Error(w, fmt.Sprintf("%v", err), http.StatusBadRequest)
14 } else {
15 // Empty response JSON means success with no body
16 w.Header().Set("Content-Type", "application/json")
17 if _, err := w.Write(result); err != nil {
18 glog.Warningf("Error writing %s HTTP response: %v", req.Command, err)
19 }
20 }
21}
从第11行可以看出,该方法又是调用requestFunc这个方法来处理请求,请求结束后通过w.Write或者是http.Error返回调用者的response。requestFunc是在cniserver的Start的方法中传入的,传入的实际上是podManager的handleCNIRequest方法,该方法位于文件pkg/network/node/pod.go#L25,内容如下:

1// Enqueue incoming pod requests from the CNI server, wait on the result,
2// and return that result to the CNI client
3func (m podManager) handleCNIRequest(request cniserver.PodRequest) ([]byte, error) {
4 glog.V(5).Infof("Dispatching pod network request %v", request)
5 m.addRequest(request)
6 result := m.waitRequest(request)
7 glog.V(5).Infof("Returning pod network request %v, result %s err %v", request, string(result.Response), result.Err)
8 return result.Response, result.Err
9}
在第5行该方法先通过addRequest方法把请求放到一个队列里面,然后调用第6行的waitRequest等待请求执行完成。
addRequest定义位于pkg/network/node/pod.go#L240, 内容如下:

1// Add a request to the podManager CNI request queue
2func (m podManager) addRequest(request cniserver.PodRequest) {
3 m.requests <- request
4}
可以看出请求被放到了m.requests这个channel里面,也就是在这里用channel做的队列。
waitRequest是从一个channel里取出结果,定义位于pkg/network/node/pod.go#L245,内容如下:

1// Wait for and return the result of a pod request
2func (m podManager) waitRequest(request cniserver.PodRequest) *cniserver.PodResult {
3 return <-request.Result
4}
刚才说了addRequest会把请求放到m.requests这个队列里面,那队列里的请求是如何被执行的呢?答案就是podManager在启动时会在一个gorotine里调用processCNIRequests这个方法,该方法会循环的从m.requests这个channel里面取出请求执行。processCNIRequests定义位于pkg/network/node/pod.go#L286,内容如下:

1// Process all CNI requests from the request queue serially. Our OVS interaction
2// and scripts currently cannot run in parallel, and doing so greatly complicates
3// setup/teardown logic
4func (m *podManager) processCNIRequests() {
5 for request := range m.requests {
6 glog.V(5).Infof("Processing pod network request %v", request)
7 result := m.processRequest(request)
8 glog.V(5).Infof("Processed pod network request %v, result %s err %v", request, string(result.Response), result.Err)
9 request.Result <- result
10 }
11 panic("stopped processing CNI pod requests!")
12}
可以看出该方法通过一个for循环不断的从m.requests里面取出请求,然后调用processRequest方法来处理请求,最后把处理的结果在放到request.Result里面由上面的waitRequest来获取。
我们来分析processRequest方法的执行逻辑,该方法定义位于pkg/network/node/pod.go#L296,内容如下:

1func (m podManager) processRequest(request cniserver.PodRequest) *cniserver.PodResult {
2 m.runningPodsLock.Lock()
3 defer m.runningPodsLock.Unlock()
4
5 pk := getPodKey(request)
6 result := &cniserver.PodResult{}
7 switch request.Command {
8 case cniserver.CNI_ADD:
9 ipamResult, runningPod, err := m.podHandler.setup(request)
10 if ipamResult != nil {
11 result.Response, err = json.Marshal(ipamResult)
12 if err == nil {
13 m.runningPods[pk] = runningPod
14 if m.ovs != nil {
15 m.updateLocalMulticastRulesWithLock(runningPod.vnid)
16 }
17 }
18 }
19 if err != nil {
20 PodOperationsErrors.WithLabelValues(PodOperationSetup).Inc()
21 result.Err = err
22 }
23 case cniserver.CNI_UPDATE:
24 vnid, err := m.podHandler.update(request)
25 if err == nil {
26 if runningPod, exists := m.runningPods[pk]; exists {
27 runningPod.vnid = vnid
28 }
29 }
30 result.Err = err
31 case cniserver.CNI_DEL:
32 if runningPod, exists := m.runningPods[pk]; exists {
33 delete(m.runningPods, pk)
34 if m.ovs != nil {
35 m.updateLocalMulticastRulesWithLock(runningPod.vnid)
36 }
37 }
38 result.Err = m.podHandler.teardown(request)
39 if result.Err != nil {
40 PodOperationsErrors.WithLabelValues(PodOperationTeardown).Inc()
41 }
42 default:
43 result.Err = fmt.Errorf("unhandled CNI request %v", request.Command)
44 }
45 return result
46}
可以看出该方法针对request.Command的三种不同取值有三部分逻辑来分别处理,我们重点分析Command等于cniserver.CNI_ADD时的逻辑,也就是前面调用openshift-sdn时传递ADD参数的处理逻辑。在Command等于cniserver.CNI_ADD部分的代码主要是调用第9行的podHandler的setup方法,该方法的定义位于pkg/network/node/pod.go#L497,内容如下:

1// Set up all networking (host/container veth, OVS flows, IPAM, loopback, etc)
2func (m podManager) setup(req cniserver.PodRequest) (cnitypes.Result, *runningPod, error) {
3 defer PodOperationsLatency.WithLabelValues(PodOperationSetup).Observe(sinceInMicroseconds(time.Now()))
4
5 pod, err := m.kClient.Core().Pods(req.PodNamespace).Get(req.PodName, metav1.GetOptions{})
6 if err != nil {
7 return nil, nil, err
8 }
9
10 ipamResult, podIP, err := m.ipamAdd(req.Netns, req.SandboxID)
11 if err != nil {
12 return nil, nil, fmt.Errorf("failed to run IPAM for %v: %v", req.SandboxID, err)
13 }
14
15 // Release any IPAM allocations and hostports if the setup failed
16 var success bool
17 defer func() {
18 if !success {
19 m.ipamDel(req.SandboxID)
20 if mappings := m.shouldSyncHostports(nil); mappings != nil {
21 if err := m.hostportSyncer.SyncHostports(Tun0, mappings); err != nil {
22 glog.Warningf("failed syncing hostports: %v", err)
23 }
24 }
25 }
26 }()
27
28 // Open any hostports the pod wants
29 var v1Pod v1.Pod
30 if err := kapiv1.Convert_core_Pod_To_v1_Pod(pod, &v1Pod, nil); err != nil {
31 return nil, nil, err
32 }
33 podPortMapping := kubehostport.ConstructPodPortMapping(&v1Pod, podIP)
34 if mappings := m.shouldSyncHostports(podPortMapping); mappings != nil {
35 if err := m.hostportSyncer.OpenPodHostportsAndSync(podPortMapping, Tun0, mappings); err != nil {
36 return nil, nil, err
37 }
38 }
39
40 vnid, err := m.policy.GetVNID(req.PodNamespace)
41 if err != nil {
42 return nil, nil, err
43 }
44
45 if err := maybeAddMacvlan(pod, req.Netns); err != nil {
46 return nil, nil, err
47 }
48
49 ofport, err := m.ovs.SetUpPod(req.SandboxID, req.HostVeth, podIP, vnid)
50 if err != nil {
51 return nil, nil, err
52 }
53 if err := setupPodBandwidth(m.ovs, pod, req.HostVeth, req.SandboxID); err != nil {
54 return nil, nil, err
55 }
56
57 m.policy.EnsureVNIDRules(vnid)
58 success = true
59 return ipamResult, &runningPod{podPortMapping: podPortMapping, vnid: vnid, ofport: ofport}, nil
60}
该方法的主要逻辑有两个,一是第10行调用m.ipamAdd获取IP,这里涉及到IPAM,后面单独分析;另一个是第49行调用ovs.SetUpPod设置OVS规则,后面也会单独分析。

至此,openshfit-sdn请求IP时cniServer的处理流程分析结束,下节我们分析cniServer如何调用IPAM插件来管理IP。

上面分析了openshfit-sdn请求IP时cniServer的处理流程,这一节我们分析cniServer调用IPAM插件来管理IP的逻辑。

IPAM
cniServer是调用IPAM插件host-local来做IP管理的,该插件位于/opt/cni/bin目录,是一个预编译的二进制可执行程序。本节将从IP的分配和释放两方面来分析cniServer跟host-local的交互流程。

IP分配
前面章节说了cniServer是调用了podManager的ipamAdd方法来获取IP的,那它又是如何同host-local插件交互的呢,我们来展开分析。
ipamAdd方法的定义位于pkg/network/node/pod.go#L422, 内容如下:

1// Run CNI IPAM allocation for the container and return the allocated IP address
2func (m podManager) ipamAdd(netnsPath string, id string) (cni020.Result, net.IP, error) {
3 if netnsPath == "" {
4 return nil, nil, fmt.Errorf("netns required for CNI_ADD")
5 }
6
7 args := createIPAMArgs(netnsPath, m.cniBinPath, cniserver.CNI_ADD, id)
8 r, err := invoke.ExecPluginWithResult(m.cniBinPath+"/host-local", m.ipamConfig, args)
9 if err != nil {
10 return nil, nil, fmt.Errorf("failed to run CNI IPAM ADD: %v", err)
11 }
12
13 // We gave the IPAM plugin 0.2.0 config, so the plugin must return a 0.2.0 result
14 result, err := cni020.GetResult(r)
15 if err != nil {
16 return nil, nil, fmt.Errorf("failed to parse CNI IPAM ADD result: %v", err)
17 }
18 if result.IP4 == nil {
19 return nil, nil, fmt.Errorf("failed to obtain IP address from CNI IPAM")
20 }
21
22 return result, result.IP4.IP.IP, nil
23}
上面代码第7行先通过createIPAMArgs方法构建一个参数变量args,变量定义如下:

1struct {
2 Command string
3 ContainerID string
4 NetNS string
5 PluginArgs [][2]string
6 PluginArgsStr string
7 IfName string
8 Path string
9}
构建后的变量的Command的值是“ADD”,这样在调用host-local时就会执行ADD相关的操作。
第8行通过invoke.ExecPluginWithResult来调用执行host-local插件,传入了上面创建的参数变量args,同时传入了一个变量ipamConfig,ipamConfig里面包含了pod所在node的子网相关配置以及一些host-local插件的配置,内容类似如下:

1{
2 "cniVersion":"0.3.1",
3 "name":"examplenet",
4 "ipam":{
5 "type":"host-local",
6 "ranges":[
7 [
8 {
9 "subnet":"203.0.113.0/24"
10 }
11 ]
12 ],
13 "dataDir":"/tmp/cni-example"
14 }
15}
调用host-local类似如下命令:

1echo '{ "cniVersion": "0.3.1", "name": "examplenet", "ipam": { "type": "host-local", "ranges": [ [{"subnet": "203.0.113.0/24"}]], "dataDir": "/tmp/cni-example" } }' | CNI_COMMAND=ADD CNI_CONTAINERID=example CNI_NETNS=/proc/48776/ns/net CNI_IFNAME=eth0 CNI_PATH=/opt/cni/bin /opt/cni/bin/host-local
调用返回的resut的值类似:

1{
2 "ips":[
3 {
4 "version":"4",
5 "address":"203.0.113.2/24",
6 "gateway":"203.0.113.1"
7 }
8 ]
9}
获取的IP信息以及网关信息在上面代码的第22行返回给调用者,也就是第三节中分析的podManager的setup方法的第10行。

IP释放

当cniServer接收到释放IP的请求时,会调用podManager的ipamDel方法,定义位于pkg/network/node/pod.go#L445,内容如下:

1// Run CNI IPAM release for the container
2func (m *podManager) ipamDel(id string) error {
3 args := createIPAMArgs("", m.cniBinPath, cniserver.CNI_DEL, id)
4 err := invoke.ExecPluginWithoutResult(m.cniBinPath+"/host-local", m.ipamConfig, args)
5 if err != nil {
6 return fmt.Errorf("failed to run CNI IPAM DEL: %v", err)
7 }
8 return nil
9}

该方法的逻辑跟ipamAdd一样,都是通过调用host-local插件来完成相应的操作,不同的是该方法在调用时传入了一个Command等于CNI_DEL的args,这样在调用host-local时就会执行IP释放的相关操作。
host-local会把所有已经分配过的IP记录到本地,也就是ipamConfig配置的dataDir目录下,在openshit环境下是记录到/var/lib/cni/networks/openshift-sdn目录下。目录下的内容类似如下:

1[root@master227 ~]# ls /var/lib/cni/networks/openshift-sdn
210.128.0.114 10.128.0.116 last_reserved_ip.0
3[root@master227 ~]#
上面列出的每一个以ip命名的文件都代表一个已经分配的IP,它的内容是该IP所在的pod的ID. 内容类似如下:

1[root@master227 ~]# cat /var/lib/cni/networks/openshift-sdn/10.128.0.114
27a1c2e242c2a2d750382837b81283952ad9878ae496195560f9854935d7e4d31[root@master227 ~]#
当分配IP时,host-local会在该目录下添加一条记录,释放IP时会删除相应的记录。

关于host-local的逻辑不再作分析,后面会有单独的章节来分析,有兴趣的可以看看源码,位于https://github.com/containern...

至此,IPAM的逻辑分析结束,下一节我们分析cniServer是如何调用ovs controller来设置Pod ovs规则。

上面我们分析了cniServer是如何通过IPAM插件来管理IP,本节主要分析cniServer是如何通过ovs controller设置pod相关的ovs规则。
OVS规则设置

openshift底层的网络用的是ovs, 那么在配置好pod IP之后,又是如何设置跟pod相关的ovs规则的呢?下面作一分析。
openshift node在启动时会创建一个ovs controller,由它来完成ovs网络配置的各种操作。在第三节我们分析过,cniServer是通过调用ovs controller的SetUpPod方法来设置pod ovs规则,调用的代码位于: pkg/network/node/pod.go#L544, 内容如下:

1ofport, err := m.ovs.SetUpPod(req.SandboxID, req.HostVeth, podIP, vnid)
SetUpPod的定义位于pkg/network/node/ovscontroller.go#L267,内容如下:

1func (oc *ovsController) SetUpPod(sandboxID, hostVeth string, podIP net.IP, vnid uint32) (int, error) {
2 ofport, err := oc.ensureOvsPort(hostVeth, sandboxID, podIP.String())
3 if err != nil {
4 return -1, err
5 }
6 return ofport, oc.setupPodFlows(ofport, podIP, vnid)
7}
在上面代码的第2行,SetUpPod又调用了ensureOvsPort这个方法,该方法的定义位于pkg/network/node/ovscontroller.go#L227,内容如下:

1func (oc *ovsController) ensureOvsPort(hostVeth, sandboxID, podIP string) (int, error) {
2 return oc.ovs.AddPort(hostVeth, -1,
3 fmt.Sprintf(external-ids=sandbox="%s",ip="%s", sandboxID, podIP),
4 )
5}
如代码所示,该方法又调用了ovs的AddPort方法,我们再来分析AddPort方法。该方法的定义位于pkg/util/ovs/ovs.go#L31,内容如下:

1func (ovsif *ovsExec) AddPort(port string, ofportRequest int, properties ...string) (int, error) {
2 args := []string{"--may-exist", "add-port", ovsif.bridge, port}
3 if ofportRequest > 0 || len(properties) > 0 {
4 args = append(args, "--", "set", "Interface", port)
5 if ofportRequest > 0 {
6 args = append(args, fmt.Sprintf("ofport_request=%d", ofportRequest))
7 }
8 if len(properties) > 0 {
9 args = append(args, properties...)
10 }
11 }
12 _, err := ovsif.exec(OVS_VSCTL, args...)
13 if err != nil {
14 return -1, err
15 }
16 ofport, err := ovsif.GetOFPort(port)
17 if err != nil {
18 return -1, err
19 }
20 if ofportRequest > 0 && ofportRequest != ofport {
21 return -1, fmt.Errorf("allocated ofport (%d) did not match request (%d)", ofport, ofportRequest)
22 }
23 return ofport, nil
24}
分析上面的代码你会发现,AddPort实际上是调用了底层的ovs-vsctl命令将pod的host端的虚拟网卡加入到了ovs网桥br0上,这样br0上的流量就可以通过该网卡进入pod了。该方法的调用类似于下面的命令行,假设pod host端的网卡是veth3258a5e2:

1ovs-vsctl --may-exist add-port br0 veth3258a5e2
接着回到SetUpPod方法,在第6行中调用了setupPodFlows来设置pod IP的ovs规则,该方法的定义位于pkg/network/node/ovscontroller.go#L233,内容如下:

1func (oc *ovsController) setupPodFlows(ofport int, podIP net.IP, vnid uint32) error {
2 otx := oc.ovs.NewTransaction()
3
4 ipstr := podIP.String()
5 podIP = podIP.To4()
6 ipmac := fmt.Sprintf("00:00:xx:x/00:00:ff:ff:ff:ff", podIP[0], podIP[1], podIP[2], podIP[3])
7
8 // ARP/IP traffic from container
9 otx.AddFlow("table=20, priority=100, in_port=%d, arp, nw_data-original=%s, arp_sha=%s, actions=load:%d->NXM_NX_REG0[], goto_table:21", ofport, ipstr, ipmac, vnid)
10 otx.AddFlow("table=20, priority=100, in_port=%d, ip, nw_data-original=%s, actions=load:%d->NXM_NX_REG0[], goto_table:21", ofport, ipstr, vnid)
11 if oc.useConnTrack {
12 otx.AddFlow("table=25, priority=100, ip, nw_data-original=%s, actions=load:%d->NXM_NX_REG0[], goto_table:30", ipstr, vnid)
13 }
14
15 // ARP request/response to container (not isolated)
16 otx.AddFlow("table=40, priority=100, arp, nw_dst=%s, actions=output:%d", ipstr, ofport)
17
18 // IP traffic to container
19 otx.AddFlow("table=70, priority=100, ip, nw_dst=%s, actions=load:%d->NXM_NX_REG1[], load:%d->NXM_NX_REG2[], goto_table:80", ipstr, vnid, ofport)
20
21 return otx.Commit()
22}
在上面代码的第9行到第19行,分别调用了AddFlow来设置各种ovs规则,第9行到第10行设置了从pod出去的ARP/IP流量的规则,第16行设置了进入POD的ARP流量规则,第19行设置了进入POD的IP流量规则。 AddFlow实际上是调用了命令行工具ovs-ofctl来设置各种ovs规则。关于这些规则的详细内容不再作分析,感兴趣的同学可以自行研究。

至此,ovs规则的设置流程分析完毕,openshit pod网络配置的流程也全部分析完毕。

查看原文

赞 0 收藏 0 评论 0

优云数智 发布了文章 · 2018-09-01

Ceph 开发者月报 2018-08

图片描述

对象存储
块存储
统一存储层
集群管理
工具库
本月提交情况

本篇为 2018 年度《Ceph 开发者月报》专栏的第八篇,在《Ceph 开发者月报》中,我们 UMCloud 存储团队将以月度为单位,为大家分享当月 Ceph 社区的有趣的提交和重要的变更,方便大家即刻掌握一手 Ceph 社区开发资料。

每篇将以对象存储、块存储、统一存储层、集群管理、基础库等模块组织,为大家一一介绍。

本期看点:

mgr 引入编排模块,这个模块所有的功能直接使用k8s的命令都可以完成,所以mgr的编排模块对于通过k8s来管理Ceph集群这件事来说就是由把用户接口放回Ceph中了,预期之后rook就不需要再加rookctl之类的用户接口了。

对象存储
cors 规则新增数量限制

rgw: cors rules num limit (https://github.com/ceph/ceph/...

为 cors 规则增加了数量限制,默认情况下,和 AWS S3 保持一致,最多 100 条。

块存储
迁移存储卷镜像 downtime 最小化

librbd: support migrating images with minimal downtime
(https://github.com/ceph/ceph/...

社区当前针对块存储的工作重心之一是实现能够在客户端用户无感知的情况下,对底层的存储卷镜像进行迁移。上面的提交是社区第一阶段的工作,实现了在迁移存储卷镜像的处理过程中,将无法对外提供服务的时间最小化。

统一存储层
允许对对象的 data_digest 进行修复

osd: Allow repair of an object with a bad data_digest in object_info on all replicas
(https://github.com/ceph/ceph/...

在上面的提交中,社区实现了当一个对象的所有副本的 data_digest 信息有问题时,支持对其进行修复。

废弃 osd rm 并为删除 osd 引入安全检查机制

mon,mgr: add safety check to ‘osd destroy’; deprecate ‘osd rm’
(https://github.com/ceph/ceph/...

在上面的提交中,社区废弃了

ceph osd rm

命令,取而代之,可以使用

ceph osd destroy

ceph osd purge
两个命令来对 osd 进行删除。同时,在上面的提交中,社区为删除 osd 操作新增了安全检查机制,可以在进行删除 osd 操作之前执行

ceph osd safe-to-destroy
命令来对该操作的安全性进行检查。

支持从网络层面对 client 用户的访问权限进行限制

mon: allow mon cap to be limited to a CIDR network
(https://github.com/ceph/ceph/...

osd 容量信息细化统计

osd: break down osd usage into data, omap, metadata buckets
(https://github.com/ceph/ceph/...

在上面的提交中,社区对 OSD 的使用容量信息进行了细化统计处理,支持分别统计显示 data, omap, metadata 的容量使用信息。

集群管理
mgr 新增 devicehealth 插件

mgr/devicehealth: respond to imminent device failures
(https://github.com/ceph/ceph/...

在上面的提交中,社区为 mgr 实现了 devicehealth 插件,从而支持:

– 当检测到有设备 (通常指磁盘) 即将发生故障时,能够提前抛出健康告警;

– 当检测到有设备 (通常指磁盘) 即将发生故障时,能够自动将该磁盘对应的 OSD 标记为 out,并对其中所存储的数据进行迁移。

通过执行如下命令来启用 devicehealth 模块:

$ ceph mgr module enable devicehealth
默认情况下,该模块是处于启用状态。对设备健康数据的采集包括如下几种模式:

  • 采集所有设备的健康数据指标

$ ceph device scrape-health-metrics

  • 采集某一特定设备的健康数据指标

$ ceph device scrape-health-metrics <device-id>

  • 采集某一特定 daemon 对应的所有设备的健康数据指标

$ ceph device scrape-daemon-health-metrics <who>

默认情况下,devicehealth 模块会周期性地对系统中的所有设备进行健康检查。可以通过

mgr/devicehealth/enable_monitoring

选项来禁用这一处理。另外,可以通过

mgr/devicehealth/warn_threshold

选项来控制 devicehealth 模块抛出健康告警到设备真正发生故障之间的窗口期的长度。

可以通过

mgr/devicehealth/self_heal

选项的启停来控制 devicehealth 模块是否会自动将即将发生故障的 osd 标记为 out,并进行相关数据的迁移操作。该选项默认为启用状态。在该选项的基础上,还可以通过

mgr/devicehealth/mark_out_threshold

选项来控制 devicehealth 模块自动将一个 osd 标记为 out 到该设备真正发生故障之间的窗口期的长度。

mgr 新增 orchestrator 相关的命令行工具和 rook 模块

mgr: orchestrator interface and experimental Rook module
(https://github.com/ceph/ceph/...

在上面的提交中,社区为 mgr 新增了 orchestrator 相关的命令行工具和 rook 模块。

支持通过 Restful API 来直接从后端修改 dashboard 的配置

mgr/dashboard: Add backend support for changing dashboard configuration settings via the REST API
(https://github.com/ceph/ceph/...

工具库
新增 ceph-crash 服务

add ceph-crash service (https://github.com/ceph/ceph/...

在上面的提交中,社区为 Ceph 新增了 ceph-crash 服务,与 mgr 的 crash 插件结合,对 crash dumps 信息进行监控和分析处理。

本月提交情况

查看原文

赞 0 收藏 0 评论 0

优云数智 发布了文章 · 2018-08-29

OpenFlow(OVS)下的“路由技术”

前言

熟悉这款设备的同学,应该也快到不惑之年了吧!这应该是Cisco最古老的路由器了。上个世纪80年代至今,路由交换技术不断发展,但是在这波澜壮阔的变化之中,总有一些东西在嘈杂的机房内闪闪发光,像极了工程师的头顶,充满了智慧!

Cisco“古董”路由器

本文主要描述了一种将三层路由变成二层交换转发(以及二层转发变成三层路由)的实现方式,以应对OVS(OpenFlow)跨网段路由复杂的问题;当然技术本身是客观的,具体应用还要看场景。

随着SDN技术不断“发展”,玩路由器交换机的变成了“传统网工”,搞控制器、转发器的才算是正常工作,当然任何新技术的掌握都离开对“历史”了解或者反刍;也许几年以后当有人听到一条一条的配置ACL、配置路由表是一件很不可思议的事情,因为那时所有的配置都是控制器做好模型生成配置自动下发的,点点鼠标或者写个py脚本就可以了

传统的路由交换机

OK,言归正传,我们先来了解一下传统路由、交换的区别:

交换: 一般指的是同网段内分组包的转发,转发依据:MAC地址

PC视角:当两台主机在同一个网段,PC1需要访问PC2时,PC1首先会发送arp请求报文,请求PC2的的MAC地址;收到响应后,PC1会把PC2的MAC地址封装在分组包的目的MAC的位置,然后将分组报文扔给交换机;PC2也会做类似的动作。

交换机视角:交换机会接收网段上的所有数据帧;利用接收数据帧中的源MAC地址来建立MAC地址表(源地址自学习),使用地址老化机制进行地址表维护。MAC地址表中查找数据帧中的目的MAC地址,如果找到就将该数据帧发送到相应的端口,如果找不到,就向除入端口以外的所有的端口发送;向所有端口转发广播帧和多播帧。

路由:一般指不同网段的数据包的转发,转发依据:IP路由

PC视角:当两台主机在不同的网段,PC1需要访问PC2时,PC1首先会在自己的路由表内查询PC2的IP地址对应的下一跳(一般默认是网关)地址,然后再去发送ARP报文,请求该下一跳对应的MAC地址;收到响应后,PC1会把该MAC地址封装在数据包的目的MAC的位置(注意此时的目的IP仍是PC2的IP地址,而不是下一跳IP),然后将数据报文扔给路由器;PC2也会做类似的动作。

路由器视角:当路由器收到一个IP数据包,路由器就会找出数据包的三层包头中的目的IP地址,然后拿着目的IP地址到自己的路由表中进行查询,找到“最匹配”的路由条目后,将数据包根据路由条目所指示的出接口或者下一跳IP转发出去,这就是IP路由(当然路由器还会做一些额外的工作:将数据包的三层包头的TTL减一,修改数据包的二层源MAC地址为自己出接口的MAC,修改数据包的二层目的MAC地址为下一跳的MAC);而每一台路由器都会在本地维护一个路由表(Routing Table),路由表中装在着路由器获知的路由条目,路由条目由路由前缀(路由所关联的目的地址)、路由信息的来源、出接口或者下一跳IP等元素构成;路由器通过静态配置或者动态的方式获取路由条目并维护自己的路由表。

OpenFlow的出现

当OpenFlow出现以后,路由器、交换机统一变成了转发器,转发依据:流表
OK,我们先看一下流表长啥样:

root@ubuntu:~# ovs-ofctl dump-flows br2
NXST_FLOW reply (xid=0x4):
cookie=0x0, duration=16080.313s, table=0, n_packets=1, n_bytes=42, idle_age=15691, priority=200,arp,arp_tpa=2.2.2.0/24 actions=output:100
cookie=0x0, duration=15964.186s, table=0, n_packets=1, n_bytes=42, idle_age=15691, priority=100,arp,arp_tpa=1.1.1.0/24 actions=output:1
cookie=0x0, duration=15985.113s, table=0, n_packets=5, n_bytes=490, idle_age=15692, priority=200,icmp,nw_dst=2.2.2.0/24 actions=output:100
cookie=0x0, duration=15802.910s, table=0, n_packets=5, n_bytes=490, idle_age=15692, priority=100,icmp,nw_dst=1.1.1.0/24 actions=output:1

当然有人称流表为ACL,这也可以理解,都有着强大的匹配域以及Action,流表的Pipeline可以算是其特色(性能暂时先不care);到此为止,MAC表、路由表在转发器上面已经统统看不到了,你能看到只有上面的流表。

就OVS来说,如果把Bridge配置成Secure模式,默认是没有什么流表的;如果现在我们把OVS配置成一台普通的传统二层交换机,只需要增加几条关于ARP、ICMP的流表,就可以Ping通了(可以参考以上示例),这还是比较简单的。

当然可能有些人说还有更简单的:只需把Bridge配置Standalone模式或者增加一条默认action=NORMAL的流表就可以了。但是如果这样的话,所有的流量又回到传统的二层三层转发去了,作为新时代的OVS,这符合我的个性啊,如果这样的话,这活还是交给Linux Bridge来干吧。

但是问题来了,如果把OVS配置成一台有路由器功能的转发器,这就比较困难了;因为通过上文分析路由转发过程相对来说还是比较复杂的,需要做的工作如下:

需要一个类似网关的设备(Device),来响应ARP请求:当然可以在新增OVS时自动生成的设备上配置网关地址,也可以增加单独的设备专门作为网关。
需要修改数据包的二层源目MAC地址以及三层包头的TTL:因为路由是逐跳转发的,每一跳都需要做这些工作,即使是现在通过流表转发,中间的转发器直接转发报文,到达倒数第一跳的时候还是需要把数据包的目的MAC地址修改为接受端的MAC地址。

一切皆交换的世界

在OpenFlow的世界所有的网络设备都是转发器或者称为交换机,执行简单的转发转发动作; OK,那我们能不能将跨网段访问的路由转发变换成普通的二层转发呢?答案是YES!

下面我们通过一个示例来实现这个想法:
首先我们要解决的第一个问题就是网关的问题:如何取消对网关的ARP请求?这个在Linux平台下并不是一件难事,只需一条命令:

root@ubuntu:~# ip route add 0.0.0.0/0 dev eth0 scope link
(同时注意arp_ignore需要是0或1)

Link路由是可以直接arp目标地址的,而不是arp下一跳地址。意思就是说,目标地址是属于跟本地直连的二层链路上,不跨三层。既然是不跨三层的链路,arp就可以畅行无阻,而标准中又没有规定arp协议包的请求源和请求目标必须是同一个网段的地址(甚至都没有掩码约束),所以说,一个以下的arp请求是有效的:

验证得到了响应:

细心的童鞋可以发现上面的命令实际上解决了我们的两个问题,网关的问题解决了,另外由于源主机直接请求目的主机的MAC地址,所以封装的时候也直接封装了目的主机的MAC,省去了我们在倒数第一跳修改数据包的目的MAC为目的主机的工作。

最后剩下一个问题就是防环的TTL的问题,这个处理起来也比较简单一些,我们可以在流表中加入actions=dec_ttl(1), output:100,在每一跳中自动减小TTL。

然后在接收端的PC上面做类似的操作,中间的OVS添加相关ARP以及业务流的流表,就实现了跨网段的“交换”。

Little Tips

通过以上描述,已经实现了跨网段的路由向交换的转换,另外也可以实现所谓二层交换向路由的转换,比如10.0.0.100/24 访问10.0.0.200/24,按照我们的想当然是应该走二层转发的,也就是直接请求目的主机的MAC地址,然后封装、发送;

但是由于种种原因,目的主机10.0.0.200/24可能跟源主机是跨三层网络的,那现在怎么办呢?OK,可以在源主机上面增加一条明细路由把10.0.0.200/24指向默认网关,在目的主机上面增加一条明细路由把10.0.0.100/24指向默认网关,然后再ping一下,有木有看到自己的嘴角上扬呢!

交换机本就应该做二层转发的事情,其他的分布式出去吧!

查看原文

赞 3 收藏 1 评论 0

优云数智 发布了文章 · 2018-08-28

解析 | OpenShift源码简析之pod网络配置(一)

openshift底层是通过kubelet来管理pod,kubelet通过CNI插件来配置pod网络.openshift node节点在启动的时会在一个goroutine中启动kubelet, 由kubelet来负责pod的管理工作。

本文主要从源码的角度入手,简单分析在openshift环境下kubelet是如何通过调用openshift sdn插件来配置pod网络。

我们先看一张pod网络配置的流程图,如下:

接下来根据流程图对各部分代码进行分析:
创建POD

当kubelet接受到pod创建请求时,会调用底层的docker来创建pod。调用入口位于pkg/kubelet/kuberuntime/kuberuntime_manager.go#L643,代码如下:

1podSandboxID, msg, err = m.createPodSandbox(pod, podContainerChanges.Attempt)
如上所示,kubelet是通过调用createPodSandbox这个方法来创建pod,该方法的定义位于pkg/kubelet/kuberuntime/kuberuntime_sandbox.go#L35内容如下:

1// createPodSandbox creates a pod sandbox and returns (podSandBoxID, message, error).
2func (m kubeGenericRuntimeManager) createPodSandbox(pod v1.Pod, attempt uint32) (string, string, error) {
3 podSandboxConfig, err := m.generatePodSandboxConfig(pod, attempt)
4 if err != nil {
5 message := fmt.Sprintf("GeneratePodSandboxConfig for pod %q failed: %v", format.Pod(pod), err)
6 glog.Error(message)
7 return "", message, err
8 }
9
10 // Create pod logs directory
11 err = m.osInterface.MkdirAll(podSandboxConfig.LogDirectory, 0755)
12 if err != nil {
13 message := fmt.Sprintf("Create pod log directory for pod %q failed: %v", format.Pod(pod), err)
14 glog.Errorf(message)
15 return "", message, err
16 }
17
18 podSandBoxID, err := m.runtimeService.RunPodSandbox(podSandboxConfig)
19 if err != nil {
20 message := fmt.Sprintf("CreatePodSandbox for pod %q failed: %v", format.Pod(pod), err)
21 glog.Error(message)
22 return "", message, err
23 }
24
25 return podSandBoxID, "", nil
26}
该方法首先会调用generatePodSandboxConfig来生成pod sandbox配置文件,然后调用MkdirAll方法来创建pod的日志目录,最后调用RunPodSandbox来完成具体的pod创建工作。

RunPodSandbox方法位于pkg/kubelet/dockershim/docker_sandbox.go#L79, 内容如下:

1// RunPodSandbox creates and starts a pod-level sandbox. Runtimes should ensure
2// the sandbox is in ready state.
3// For docker, PodSandbox is implemented by a container holding the network
4// namespace for the pod.
5// Note: docker doesn't use LogDirectory (yet).
6func (ds dockerService) RunPodSandbox(ctx context.Context, r runtimeapi.RunPodSandboxRequest) (*runtimeapi.RunPodSandboxResponse, error) {
7 config := r.GetConfig()
8
9 // Step 1: Pull the image for the sandbox.
10 image := defaultSandboxImage
11 podSandboxImage := ds.podSandboxImage
12 if len(podSandboxImage) != 0 {
13 image = podSandboxImage
14 }
15
16 // NOTE: To use a custom sandbox image in a private repository, users need to configure the nodes with credentials properly.
17 // see: http://kubernetes.io/docs/use...
18 // Only pull sandbox image when it's not present - v1.PullIfNotPresent.
19 if err := ensureSandboxImageExists(ds.client, image); err != nil {
20 return nil, err
21 }
22
23 // Step 2: Create the sandbox container.
24 createConfig, err := ds.makeSandboxDockerConfig(config, image)
25 if err != nil {
26 return nil, fmt.Errorf("failed to make sandbox docker config for pod %q: %v", config.Metadata.Name, err)
27 }
28 createResp, err := ds.client.CreateContainer(*createConfig)
29 if err != nil {
30 createResp, err = recoverFromCreationConflictIfNeeded(ds.client, *createConfig, err)
31 }
32
33 if err != nil || createResp == nil {
34 return nil, fmt.Errorf("failed to create a sandbox for pod %q: %v", config.Metadata.Name, err)
35 }
36 resp := &runtimeapi.RunPodSandboxResponse{PodSandboxId: createResp.ID}
37
38 ds.setNetworkReady(createResp.ID, false)
39 defer func(e *error) {
40 // Set networking ready depending on the error return of
41 // the parent function
42 if *e == nil {
43 ds.setNetworkReady(createResp.ID, true)
44 }
45 }(&err)
46
47 // Step 3: Create Sandbox Checkpoint.
48 if err = ds.checkpointHandler.CreateCheckpoint(createResp.ID, constructPodSandboxCheckpoint(config)); err != nil {
49 return nil, err
50 }
51
52 // Step 4: Start the sandbox container.
53 // Assume kubelet's garbage collector would remove the sandbox later, if
54 // startContainer failed.
55 err = ds.client.StartContainer(createResp.ID)
56 if err != nil {
57 return nil, fmt.Errorf("failed to start sandbox container for pod %q: %v", config.Metadata.Name, err)
58 }
59
60 // Rewrite resolv.conf file generated by docker.
61 // NOTE: cluster dns settings aren't passed anymore to docker api in all cases,
62 // not only for pods with host network: the resolver conf will be overwritten
63 // after sandbox creation to override docker's behaviour. This resolv.conf
64 // file is shared by all containers of the same pod, and needs to be modified
65 // only once per pod.
66 if dnsConfig := config.GetDnsConfig(); dnsConfig != nil {
67 containerInfo, err := ds.client.InspectContainer(createResp.ID)
68 if err != nil {
69 return nil, fmt.Errorf("failed to inspect sandbox container for pod %q: %v", config.Metadata.Name, err)
70 }
71
72 if err := rewriteResolvFile(containerInfo.ResolvConfPath, dnsConfig.Servers, dnsConfig.Searches, dnsConfig.Options); err != nil {
73 return nil, fmt.Errorf("rewrite resolv.conf failed for pod %q: %v", config.Metadata.Name, err)
74 }
75 }
76
77 // Do not invoke network plugins if in hostNetwork mode.
78 if config.GetLinux().GetSecurityContext().GetNamespaceOptions().GetNetwork() == runtimeapi.NamespaceMode_NODE {
79 return resp, nil
80 }
81
82 // Step 5: Setup networking for the sandbox.
83 // All pod networking is setup by a CNI plugin discovered at startup time.
84 // This plugin assigns the pod ip, sets up routes inside the sandbox,
85 // creates interfaces etc. In theory, its jurisdiction ends with pod
86 // sandbox networking, but it might insert iptables rules or open ports
87 // on the host as well, to satisfy parts of the pod spec that aren't
88 // recognized by the CNI standard yet.
89 cID := kubecontainer.BuildContainerID(runtimeName, createResp.ID)
90 err = ds.network.SetUpPod(config.GetMetadata().Namespace, config.GetMetadata().Name, cID, config.Annotations)
91 if err != nil {
92 // TODO(random-liu): Do we need to teardown network here?
93 if err := ds.client.StopContainer(createResp.ID, defaultSandboxGracePeriod); err != nil {
94 glog.Warningf("Failed to stop sandbox container %q for pod %q: %v", createResp.ID, config.Metadata.Name, err)
95 }
96 }
97 return resp, err
98}
在上面代码的第19行,首先通过调用ensureSandboxImageExists方法来拉取pod infra容器的镜像,确保在infra容器创建时镜像已经在本地。该方法的定义位于pkg/kubelet/dockershim/helpers.go#L316,内容如下:

1func ensureSandboxImageExists(client libdocker.Interface, image string) error {
2 _, err := client.InspectImageByRef(image)
3 if err == nil {
4 return nil
5 }
6 if !libdocker.IsImageNotFoundError(err) {
7 return fmt.Errorf("failed to inspect sandbox image %q: %v", image, err)
8 }
9
10 repoToPull, _, _, err := parsers.ParseImageName(image)
11 if err != nil {
12 return err
13 }
14
15 keyring := credentialprovider.NewDockerKeyring()
16 creds, withCredentials := keyring.Lookup(repoToPull)
17 if !withCredentials {
18 glog.V(3).Infof("Pulling image %q without credentials", image)
19
20 err := client.PullImage(image, dockertypes.AuthConfig{}, dockertypes.ImagePullOptions{})
21 if err != nil {
22 return fmt.Errorf("failed pulling image %q: %v", image, err)
23 }
24
25 return nil
26 }
27
28 var pullErrs []error
29 for _, currentCreds := range creds {
30 authConfig := credentialprovider.LazyProvide(currentCreds)
31 err := client.PullImage(image, authConfig, dockertypes.ImagePullOptions{})
32 // If there was no error, return success
33 if err == nil {
34 return nil
35 }
36
37 pullErrs = append(pullErrs, err)
38 }
39
40 return utilerrors.NewAggregate(pullErrs)
41}
该方法会首先判断镜像在不在本地,如果已经存在于本地则直接返回,如果不存在则调用docker client拉取镜像,拉取镜像时还会处理认证相关的问题。

在拉取镜像成功后,在第20行调用了CreateContainer来创建infra容器,该方法的定义位于pkg/kubelet/dockershim/libdocker/kube_docker_client.go#L141,内容如下:

1func (d kubeDockerClient) CreateContainer(opts dockertypes.ContainerCreateConfig) (dockercontainer.ContainerCreateCreatedBody, error) {
2 ctx, cancel := d.getTimeoutContext()
3 defer cancel()
4 // we provide an explicit default shm size as to not depend on docker daemon.
5 // TODO: evaluate exposing this as a knob in the API
6 if opts.HostConfig != nil && opts.HostConfig.ShmSize <= 0 {
7 opts.HostConfig.ShmSize = defaultShmSize
8 }
9 createResp, err := d.client.ContainerCreate(ctx, opts.Config, opts.HostConfig, opts.NetworkingConfig, opts.Name)
10 if ctxErr := contextError(ctx); ctxErr != nil {
11 return nil, ctxErr
12 }
13 if err != nil {
14 return nil, err
15 }
16 return &createResp, nil
17}
该方法在第9行实际上是调用docker client来创建容器,最终也就是调用docker的remote api来创建的容器。

在创建infra容器成功之后,在代码的第55行通过调用StartContainer来启动上一步中创建成功的容器。StartContainer的定义位于pkg/kubelet/dockershim/libdocker/kube_docker_client.go#L159,内容如下:

1func (d *kubeDockerClient) StartContainer(id string) error {
2 ctx, cancel := d.getTimeoutContext()
3 defer cancel()
4 err := d.client.ContainerStart(ctx, id, dockertypes.ContainerStartOptions{})
5 if ctxErr := contextError(ctx); ctxErr != nil {
6 return ctxErr
7 }
8 return err
9}
从上面的代码第4行可以看出,跟CreateContainer类似,这一步也是通过调用docker的api接口来完成。

至此,pod创建的工作完成,从上面的分析可以看出kubelet最终是通过调用docker的接口来完成pod的创建。

这里需要说明一点,kubelet在创建pod时是先创建一个infra容器,配置好该容器的网络,然后创建真正工作的业务容器,最后再把业务容器的网络加到infra容器的网络命名空间中,相当于业务容器共享infra容器的网络命名空间。业务容器和infra容器共同组成一个pod。

接下来,我们将分析kubelet是如何通过CNI插件来为pod配置网络。

配置pod网络

在RunPodSandbox方法的第90行,调用了network plugin的SetUpPod方法来配置pod网络。该方法位于pkg/kubelet/network/plugins.go#L406,内容如下:

1func (pm *PluginManager) SetUpPod(podNamespace, podName string, id kubecontainer.ContainerID, annotations map[string]string) error {
2 defer recordOperation("set_up_pod", time.Now())
3 fullPodName := kubecontainer.BuildPodFullName(podName, podNamespace)
4 pm.podLock(fullPodName).Lock()
5 defer pm.podUnlock(fullPodName)
6
7 glog.V(3).Infof("Calling network plugin %s to set up pod %q", pm.plugin.Name(), fullPodName)
8 if err := pm.plugin.SetUpPod(podNamespace, podName, id, annotations); err != nil {
9 return fmt.Errorf("NetworkPlugin %s failed to set up pod %q network: %v", pm.plugin.Name(), fullPodName, err)
10 }
11
12 return nil
13}
该方法主要逻辑是第8行,调用plugin的SetUpPod方法,这里plugin是一个interface, 具体使用哪个plugin是由kubelet的启动参数--network-plugin决定的,openshift在启动kubelet时传递的参数是--netowr-plugin=cni,也就是调用cni插件的SetupPod方法。该方法的定义位于:pkg/kubelet/network/cni/cni.go#L208,内容如下:

1func (plugin *cniNetworkPlugin) SetUpPod(namespace string, name string, id kubecontainer.ContainerID, annotations map[string]string) error {
2 if err := plugin.checkInitialized(); err != nil {
3 return err
4 }
5 netnsPath, err := plugin.host.GetNetNS(id.ID)
6 if err != nil {
7 return fmt.Errorf("CNI failed to retrieve network namespace path: %v", err)
8 }
9
10 // Windows doesn't have loNetwork. It comes only with Linux
11 if plugin.loNetwork != nil {
12 if _, err = plugin.addToNetwork(plugin.loNetwork, name, namespace, id, netnsPath); err != nil {
13 glog.Errorf("Error while adding to cni lo network: %s", err)
14 return err
15 }
16 }
17
18 _, err = plugin.addToNetwork(plugin.getDefaultNetwork(), name, namespace, id, netnsPath)
19 if err != nil {
20 glog.Errorf("Error while adding to cni network: %s", err)
21 return err
22 }
23
24 return err
25}
该方法先调用GetNetNS找到pod所在的netnamespace的路径,该值在后续配置网络时会用到,然后如果系统是linux的话,会调用addToNetwork来配置loopback设备的网络,最后调用addToNetwork来配置pod eth0接口的网络。这里需要关注一下第18行的getDefaultNetwork这个方法,该方法的源码位于pkg/kubelet/network/cni/cni.go#L177, 内容如下:

1func (plugin cniNetworkPlugin) getDefaultNetwork() cniNetwork {
2 plugin.RLock()
3 defer plugin.RUnlock()
4 return plugin.defaultNetwork
5}
该方法返回plugin.defaultNetwork,该值最终是调用getDefaultCNINetwork方法获取,源码位于pkg/kubelet/network/cni/cni.go#L95, 内容如下:

1func getDefaultCNINetwork(pluginDir, binDir, vendorCNIDirPrefix string) (*cniNetwork, error) {
2 if pluginDir == "" {
3 pluginDir = DefaultNetDir
4 }
5 files, err := libcni.ConfFiles(pluginDir, []string{".conf", ".conflist", ".json"})
6 switch {
7 case err != nil:
8 return nil, err
9 case len(files) == 0:
10 return nil, fmt.Errorf("No networks found in %s", pluginDir)
11 }
12
13 sort.Strings(files)
14 for _, confFile := range files {
15 var confList *libcni.NetworkConfigList
16 if strings.HasSuffix(confFile, ".conflist") {
17 confList, err = libcni.ConfListFromFile(confFile)
18 if err != nil {
19 glog.Warningf("Error loading CNI config list file %s: %v", confFile, err)
20 continue
21 }
22 } else {
23 conf, err := libcni.ConfFromFile(confFile)
24 if err != nil {
25 glog.Warningf("Error loading CNI config file %s: %v", confFile, err)
26 continue
27 }
28 // Ensure the config has a "type" so we know what plugin to run.
29 // Also catches the case where somebody put a conflist into a conf file.
30 if conf.Network.Type == "" {
31 glog.Warningf("Error loading CNI config file %s: no 'type'; perhaps this is a .conflist?", confFile)
32 continue
33 }
34
35 confList, err = libcni.ConfListFromConf(conf)
36 if err != nil {
37 glog.Warningf("Error converting CNI config file %s to list: %v", confFile, err)
38 continue
39 }
40 }
41 if len(confList.Plugins) == 0 {
42 glog.Warningf("CNI config list %s has no networks, skipping", confFile)
43 continue
44 }
45 confType := confList.Plugins[0].Network.Type
46
47 // Search for vendor-specific plugins as well as default plugins in the CNI codebase.
48 vendorDir := vendorCNIDir(vendorCNIDirPrefix, confType)
49 cninet := &libcni.CNIConfig{
50 Path: []string{vendorDir, binDir},
51 }
52 network := &cniNetwork{name: confList.Name, NetworkConfig: confList, CNIConfig: cninet}
53 return network, nil
54 }
55 return nil, fmt.Errorf("No valid networks found in %s", pluginDir)
56}
该方法首先读取pluginDir也就是/etc/cni/net.d目录下的所有以".conf",".conflist"或者是".json"结尾的配置文件,然后解析配置文件,最后生成一个cniNetwork的对象,该对象包含了cni插件的名称,cni插件的配置等等。cniNetwork的定义位于pkg/kubelet/network/cni/cni.go#L58,内容如下:

1type cniNetwork struct {
2 name string
3 NetworkConfig *libcni.NetworkConfigList
4 CNIConfig libcni.CNI
5}

openshift node节点在启动时,会在/etc/cni/net.d目录下写入配置文件80-openshift-network.conf,内容如下:

1{
2 “cniVersion”: “0.2.0”,
3 “name”: “openshift-sdn”,
4 “type”: “openshift-sdn”
5}
所以上面的getDefaultCNINetwork的执行实际上是读取到了openshift sdn插件的相关配置。

接下来回到addToNetwork方法,该方法的定义位pkg/kubelet/network/cni/cni.go#L248, 内容如下:

1func (plugin cniNetworkPlugin) addToNetwork(network cniNetwork, podName string, podNamespace string, podSandboxID kubecontainer.ContainerID, podNetnsPath string) (cnitypes.Result, error) {
2 rt, err := plugin.buildCNIRuntimeConf(podName, podNamespace, podSandboxID, podNetnsPath)
3 if err != nil {
4 glog.Errorf("Error adding network when building cni runtime conf: %v", err)
5 return nil, err
6 }
7
8 netConf, cniNet := network.NetworkConfig, network.CNIConfig
9 glog.V(4).Infof("About to add CNI network %v (type=%v)", netConf.Name, netConf.Plugins[0].Network.Type)
10 res, err := cniNet.AddNetworkList(netConf, rt)
11 if err != nil {
12 glog.Errorf("Error adding network: %v", err)
13 return nil, err
14 }
15
16 return res, nil
17}
该方法首先调用buildCNIRuntimeConf生成一个RuntimeConf对象,该对象在调用cni插件时会用到,定义位于vendor/github.com/containernetworking/cni/libcni/api.go#L26,内容如下:

1type RuntimeConf struct {
2 ContainerID string
3 NetNS string
4 IfName string
5 Args [][2]string
6 // A dictionary of capability-specific data passed by the runtime
7 // to plugins as top-level keys in the 'runtimeConfig' dictionary
8 // of the plugin's stdin data. libcni will ensure that only keys
9 // in this map which match the capabilities of the plugin are passed
10 // to the plugin
11 CapabilityArgs map[string]interface{}
12}
ContainerID是创建的pod的ID;NetNS是pod所在的netspace的path,这个在之前有提到过;IfName在kubelet里是定义为一个常量值eth0;Args包含了一些pod相关的参数;CapabilityArgs包含了pod的portmappging的配置。

接着分析addToNetwork,该方法的第10行调用了cniNet的AddNetworkList方法,该方法的定义位于vendor/github.com/containernetworking/cni/libcni/api.go#L123,内容如下:

1func (c CNIConfig) AddNetworkList(list NetworkConfigList, rt *RuntimeConf) (types.Result, error) {
2 var prevResult types.Result
3 for _, net := range list.Plugins {
4 pluginPath, err := invoke.FindInPath(net.Network.Type, c.Path)
5 if err != nil {
6 return nil, err
7 }
8
9 newConf, err := buildOneConfig(list, net, prevResult, rt)
10 if err != nil {
11 return nil, err
12 }
13
14 prevResult, err = invoke.ExecPluginWithResult(pluginPath, newConf.Bytes, c.args("ADD", rt))
15 if err != nil {
16 return nil, err
17 }
18 }
19
20 return prevResult, nil
21}
该方法首先调用FindInPath这个方法来找到plugin的路径,FindInPath会根据CNI配置的Type在/opt/cni/bin下面找到同名的插件,然后返回插件的绝对路径。我们以openshift sdn插件的配置为例,配置的内容如下:

1{
2 “cniVersion”: “0.2.0”,
3 “name”: “openshift-sdn”,
4 “type”: “openshift-sdn”
5}
上面配置的type字段的值是openshift-sdn,也就是FindInPath会在/opt/cni/bin下查找openshift-sdn这个插件,找到后返回插件的绝对路径也就是/opt/cni/bin/opnshift-sdn。
接下来会调用ExecPluginWithResult执行刚才找到的插件,调用时传入了rutingconfig的配置,以及一个ADD参数,ADD参数表示是配置网络,CNI插件都支持ADD,DEL等参数来配置和删除网络。

上面的部分把调用openshift-sdn插件之前的流程都分析完了,下面分析openshift-sdn插件的具体调用流程。openshift-sdn插件的源码位于openshift代码库的pkg/network/sdn-cni-plugin/openshift-sdn.go文件,主要包括一下几个方法:

doCNI: 该方法用于向CNIServer发送请求,openshit node节点在启动时会启动一个cniServer, 用于跟cni plugin进行通信,通信的流程下面会分析。
CmdAdd: 用于执行ADD请求,在设置pod网络时会被调用,比如上面在调用插件时传入了ADD参数就是调用这个方法。
CmdDel: 用于执行DEL请求,在删除pod网络时会被调用。
在这里我们主要分析CmdAdd这个方法,该方法在上面的ExecPluginWithResult方法执行时被调用,CmdAdd方法的定义位于pkg/network/sdn-cni-plugin/openshift-sdn.go#L118,内容如下:

1func (p cniPlugin) CmdAdd(args skel.CmdArgs) error {
2 req := newCNIRequest(args)
3 config, err := cniserver.ReadConfig(cniserver.CNIServerConfigFilePath)
4 if err != nil {
5 return err
6 }
7
8 var hostVeth, contVeth net.Interface
9 err = ns.WithNetNSPath(args.Netns, func(hostNS ns.NetNS) error {
10 hostVeth, contVeth, err = ip.SetupVeth(args.IfName, int(config.MTU), hostNS)
11 if err != nil {
12 return fmt.Errorf("failed to create container veth: %v", err)
13 }
14 return nil
15 })
16 if err != nil {
17 return err
18 }
19 result, err := p.doCNIServerAdd(req, hostVeth.Name)
20 if err != nil {
21 return err
22 }
23
24 // current.NewResultFromResult and ipam.ConfigureIface both think that
25 // a route with no gateway specified means to pass the default gateway
26 // as the next hop to ip.AddRoute, but that's not what we want; we want
27 // to pass nil as the next hop. So we need to clear the default gateway.
28 result020, err := types020.GetResult(result)
29 if err != nil {
30 return fmt.Errorf("failed to convert IPAM result: %v", err)
31 }
32 defaultGW := result020.IP4.Gateway
33 result020.IP4.Gateway = nil
34
35 result030, err := current.NewResultFromResult(result020)
36 if err != nil || len(result030.IPs) != 1 || result030.IPs[0].Version != "4" {
37 return fmt.Errorf("failed to convert IPAM result: %v", err)
38 }
39
40 // Add a sandbox interface record which ConfigureInterface expects.
41 // The only interface we report is the pod interface.
42 result030.Interfaces = []*current.Interface{
43 {
44 Name: args.IfName,
45 Mac: contVeth.HardwareAddr.String(),
46 Sandbox: args.Netns,
47 },
48 }
49 result030.IPs[0].Interface = current.Int(0)
50
51 err = ns.WithNetNSPath(args.Netns, func(hostNS ns.NetNS) error {
52 // Set up eth0
53 if err := ip.SetHWAddrByIP(args.IfName, result030.IPs[0].Address.IP, nil); err != nil {
54 return fmt.Errorf("failed to set pod interface MAC address: %v", err)
55 }
56 if err := ipam.ConfigureIface(args.IfName, result030); err != nil {
57 return fmt.Errorf("failed to configure container IPAM: %v", err)
58 }
59
60 // Set up lo
61 link, err := netlink.LinkByName("lo")
62 if err == nil {
63 err = netlink.LinkSetUp(link)
64 }
65 if err != nil {
66 return fmt.Errorf("failed to configure container loopback: %v", err)
67 }
68
69 // Set up macvlan0 (if it exists)
70 link, err = netlink.LinkByName("macvlan0")
71 if err == nil {
72 err = netlink.LinkSetUp(link)
73 if err != nil {
74 return fmt.Errorf("failed to enable macvlan device: %v", err)
75 }
76
77 // A macvlan can't reach its parent interface's IP, so we need to
78 // add a route to that via the SDN
79 var addrs []netlink.Addr
80 err = hostNS.Do(func(ns.NetNS) error {
81 parent, err := netlink.LinkByIndex(link.Attrs().ParentIndex)
82 if err != nil {
83 return err
84 }
85 addrs, err = netlink.AddrList(parent, netlink.FAMILY_V4)
86 return err
87 })
88 if err != nil {
89 return fmt.Errorf("failed to configure macvlan device: %v", err)
90 }
91 for _, addr := range addrs {
92 route := &netlink.Route{
93 Dst: &net.IPNet{
94 IP: addr.IP,
95 Mask: net.CIDRMask(32, 32),
96 },
97 Gw: defaultGW,
98 }
99 if err := netlink.RouteAdd(route); err != nil {
100 return fmt.Errorf("failed to add route to node IP: %v", err)
101 }
102 }
103
104 // Add a route to service network via SDN
105 _, serviceIPNet, err := net.ParseCIDR(config.ServiceNetworkCIDR)
106 if err != nil {
107 return fmt.Errorf("failed to parse ServiceNetworkCIDR: %v", err)
108 }
109 route := &netlink.Route{
110 Dst: serviceIPNet,
111 Gw: defaultGW,
112 }
113 if err := netlink.RouteAdd(route); err != nil {
114 return fmt.Errorf("failed to add route to service network: %v", err)
115 }
116 }
117
118 return nil
119 })
120 if err != nil {
121 return err
122 }
123
124 return result.Print()
125}
该方法首先会调用SetupVeth(第10行)来创建一个虚拟设备对,设备对的一端连到主机上,也就是我们在主机上看到的类似veth3258a5e2这样的虚拟网卡,另一端放到容器里,也就是我们在容器里看到的eth0。

虚拟设备对的两端是相通的。然后调用doCNIServerAdd(第19行)来向cniserver请求IP,请求IP的流程后面分析,请求到IP成功之后在第53行调用SetHWAddrByIP和56行的ConfigureIface配置eth0的IP地址和mac地址。然后从61行到64行是配置loopback接口。还有一部分关于macvlan接口的配置,这里不做分析。

本节的分析到此结束,下一小节我们分析openshift-sdn获取IP时cniServer的处理流程。

查看原文

赞 0 收藏 0 评论 0

优云数智 发布了文章 · 2018-08-24

Istio在UAEK中的实践改造之路

为什么需要ServiceMesh
UCloud App Engine on Kubernetes(后简称“UAEK”)是UCloud内部打造的一个基于Kubernetes的,具备高可用、跨机房容灾、自动伸缩、立体监控、日志搜集和简便运维等特性计算资源交付平台。UAEK旨在利用容器技术提高内部研发运维效率,让开发能将更多的精力投入在业务研发本身,同时,让运维能更从容应对资源伸缩、灰度发布、版本更迭、监控告警等日常工作。

考虑到Kubernetes本来就是为自动部署、伸缩和容器化而生,再加上UCloud UAEK团队完成IPv6组网调研和设计实现后,一个成熟的容器管理平台很快正式在北京二地域的多个可用区上线了。相比于过去申请管理虚拟机部署应用服务,Kubernetes确实带来了实实在在的便利,例如方便灵活的自动伸缩以及触手可及的微服务架构,只需简单配置即可实现跨可用区容灾等。

然而,微服务化又为系统架构带来许多新的问题,例如服务发现、监控、灰度控制、过载保护、请求调用追踪等。大家已经习惯自行运维一组Zookeeper集群用以实现服务发现和客户端负载均衡,使用UAEK后能否免去运维Zookeeper的工作?为了监控业务运行状态,大家都需要在代码里加上旁路上报逻辑,使用UAEK是否能无侵入零耦合地实现监控上报?

此外,过去很多系统模块间调用缺少熔断保护策略,波峰流量一打就瘫,使用UAEK是否能帮助业务方免去大规模改造呢?过去排查问题,尤其是调用耗时环节排查总是费时费力,使用UAEK能否为定位瓶颈提供方便的工具?

显然,仅凭一个稳定的Kubernetes平台不足以解决这些问题。因此,在UAEK立项之初,团队就把ServiceMesh作为一个必须实现的目标,任何在UAEK上部署的TCP后台服务,都能享受到ServiceMesh带来的这些特性:
SideCar模式部署,零侵入,微服务治理代码与业务代码完全解耦;
与Kubernetes平台融合的服务发现机制和负载均衡调度;
提供灵活,实时,无需重启、能根据7层业务信息进行流量灰度管理功能;
提供统一抽象数据上报API层,用于实现监控和访问策略控制;
使用分布式请求链路追踪系统,快速追溯Bug,定位系统性能瓶颈;
过载保护机制,能在请求量超过系统设计容量时自动触发熔断;
能在服务上线前提供故障模拟注入演习剧本,提前进行故障处理演练;

这样,使用UAEK部署应用服务后,即可从小范围按账号灰度上线开始,通过陆续地监控观察,轻松掌握版本异常回退、扩大灰度范围、全量发布、过载保护、异常请求定位追踪等信息。

2为什么是Istio?
关于ServiceMesh的实现,我们重点考察了Istio。通过前期的调研和测试,我们发现Istio的几个特性能很好满足UAEK的需求:
完美支持Kubernetes平台;
控制面和数据转发面分离;
Sidecar部署,掌控所有服务间调用流量,无上限的控制力;
使用Envoy作为Sidecar实现,Envoy使用C++11开发,基于事件驱动和多线程机制运行,性能好并发能力强,媲美NGINX;
对业务的代码和配置文件零侵入;
配置简单,操作方便,API完善。

整个服务网格分成控制面板和数据面两大部分。数据面指的就是注入到应用Pod中的Envoy容器,它负责代理调度模块间的所有流量。控制面分为Pilot,Mixer和Citadel三大模块,具体功能如下:
Pilot负责向Kubernetes API获取并Watch整个集群的服务发现信息,并向Envoy下发集群服务发现信息和用户定制的路由规则策略。
Mixer分为Policy和Telemetry两个子模块。Policy用于向Envoy提供准入策略控制,黑白名单控制,QPS流速控制服务;Telemetry为Envoy提供了数据上报和日志搜集服务,以用于监控告警和日志查询。
Citadel为服务和用户提供认证和鉴权、管理凭据和 RBAC。

此外Istio为运维人员提供了一个叫istioctl的命令行工具,类似kubernetes的kubectl。运维编写好路由规则yaml文件后,使用istioctl即可向集群提交路由规则。

Istio整体工作的原理和流程细节非常复杂,所涉及到的技术栈有一定的深度和广度。这里只概括一下大体过程:
运维人员使用istioctl或者调用API向控制层创建修改路由规则策略。
Pilot向Kube APIServer获取并watch集群服务发现信息。
部署应用程序时,Istio会在pod的部署配置中注入Envoy容器,Envoy会通过iptables nat redirect劫持代理pod中的全部TCP流量。
Envoy会实时从Pilot更新集群的服务发现信息和路由规则策略,并根据这些信息智能调度集群内的流量。
Envoy会在每次请求发送前向Mixer Policy发送Check请求检查该请求是否收策略限制或者配额限制,每次请求接收后会向Mixer Telemetry上报本次请求的基本信息,如调用是否成功、返回状态码、耗时数据。
Citadel实现了双向TLS客户端证书生成与注入,服务端密钥和证书的下发注入,以及K8S RBAC访问控制。

3Istio在UAEK环境下的改造之路
经过上述的调研和与一系列测试,UAEK团队充分认可Istio的设计理念和潜在价值,希望通过利用Istio丰富强大的微服务治理功能吸引更多的内部团队将服务迁移到UAEK环境中。

然而,事实上,在UAEK上接入Istio的过程并非一帆风顺。最早开始调研Istio的时候,Istio还在0.6版本,功能并不完善,在UAEK环境中无法开箱即用。

IPv6问题的解决
我们首先碰到的问题是,UAEK是一个纯IPv6网络环境,而Istio对IPv6流量的支持并不完备,部分组件甚至无法在IPv6环境下部署。

在介绍具体改造案例之前,先了解下IstioSidecar是如何接管业务程序的流量。

如上图所描述,Istio会向应用Pod注入两个容器:proxy-init容器和envoy容器。proxy-init容器通过初始化iptables设置,将所有的TCP层流量通过nat redirect重定向到Envoy监听的15001端口。以入流量为例,Envoy的服务端口接收到被重定向到来的TCP连接后,通过getsocketopt(2)系统调用,使用SO_ORIGINAL_DST参数找到该TCP连接的真实目的地IP地址,并将该请求转发到真实目的IP。

然而,我们发现在IPv6环境下,Envoy无法劫持Pod的流量。通过抓包观察和追溯源码发现,Pod启动的时候,首先会运行一个iptables初始化脚本,完成pod内的nat redirect配置,将容器内的TCP出入流量都劫持到Envoy的监听端口中,但这个初始化脚本没有ip6tables的对应操作并且discard了所有IPv6流量,因此我们修改了初始化脚本,实现了IPv6的流量劫持。

一波刚平,一波又起。完成IPv6流量劫持后,我们发现所有访问业务服务端口的TCP流量都被Envoy重置,进入Envoy容器中发现15001端口并没有开启。追溯Envoy和Pilot源码发现,Pilot给Envoy下发的listen地址为0:0:0:0:15001,这是个IPv4地址,我们需要Envoy监听地址的为[::0]:15000,于是继续修改Pilot源码。

经过上述努力,应用服务端程序Pod终于能成功Accept我们发起的TCP连接。但很快,我们的请求连接就被服务端关闭,客户端刚连接上就立刻收到TCP FIN分节,请求依然失败。通过观察Envoy的运行日志,发现Envoy接收了TCP请求后,无法找到对应的4层流量过滤器(Filter)。

深入跟进源码发现,Envoy需要通过getsocketopt(2)系统调用获取被劫持的访问请求的真实目的地址, 但在IPv6环境下Envoy相关的实现存在bug,如下代码所示。由于缺少判定socket fd的类型, getsocketopt(2)传入的参数是IPv4环境下的参数,因此Envoy无法找到请求的真实目的地址,遂报错并立刻关闭了客户端连接。

发现问题后,UAEK团队立刻修改Envoy源码,完善了getsocketopt(2) 的SO_ORIGINAL_DST选项的IPv6兼容性,然后将这一修改提交到Envoy开源社区,随后被社区合并到当前的Master分支中,并在Istio1.0的Envoy镜像中得到更新使用。

到此为止,Istio SideCar终于能在UAEK IPv6环境下正常调度服务间的访问流量了。

此外,我们还发现Pilot、Mixer等模块在处理IPv6格式地址时出现数组越界、程序崩溃的情况,并逐一修复之。

性能评估
Istio1.0发布之前,性能问题一直是业界诟病的焦点。我们首先考察了增加了Envoy后,流量多了一层复制,并且请求发起前需要向Mixer Policy进行一次Check请求,这些因素是否会对业务产生不可接收的延迟。经过大量测试,我们发现在UAEK环境下会比不使用Istio时增加5ms左右的延迟,对内部大部分服务来说,这完全可以接受。

随后,我们重点考察了整个Istio Mesh的架构,分析下来结论是,Mixer Policy和Mixer Telemetry很容易成为整个集群的性能短板。由于Envoy发起每个请求前都需要对Policy服务进行Check请求,一方面增加了业务请求本身的延迟,一方面也给作为单点的Policy增大了负载压力。我们以Http1.1请求作为样本测试,发现当整个网格QPS达到2000-3000的时候,Policy就会出现严重的负载瓶颈,导致所有的Check请求耗时显著增大,由正常情况下的2-3ms增大到100-150ms,严重加剧了所有业务请求的耗时延迟,这个结果显然是不可接受的。

更严重的是,在Istio 0.8以及之前的版本,Policy是一个有状态的服务。一些功能,如全局的QPS Ratelimit配额控制,需要Policy单个进程记录整个Mesh的实时数据,这意味着Policy服务无法通过横向扩容实例来解决性能瓶颈。经过取舍权衡,我们目前关闭了Policy服务并裁剪了一些功能,比如QPS全局配额限制。

前面提到过,Mixer Telemetry主要负责向Envoy收集每次请求的调用情况。0.8版本的Mixer Telemetry也存在严重的性能问题。压测中发现,当集群QPS达到2000以上时,Telemetry实例的内存使用率会一路狂涨。

经过分析定位,发现Telemetry内存上涨的原因是数据通过各种后端Adapter消费的速率无法跟上Envoy上报的速率, 导致未被Adapter处理的数据快速积压在内存中。我们随即去除了Istio自带的并不实用的stdio日志搜集功能,这一问题随即得到极大缓解。幸运的是,随着Istio1.0的发布,Telemetry的内存数据积压问题得到解决,在相同的测试条件下,单个Telemetry实例至少能胜任3.5W QPS情况下的数据搜集上报。

4问题、希望与未来
历经重重问题,一路走来,一个生产环境可用的ServiceMesh终于在UAEK环境上线了。在这一过程中,也有部门内其他团队受UAEK团队影响,开始学习Istio的理念并尝试在项目中使用Istio。然而,目前的现状离我们的初心依然存在差距。

Istio依然在高速迭代中,无论是Istio本身还是Envoy Proxy,每天都在演进更新。每一次版本更新,带来的都是更为强大的功能,更为简练的API定义,同时也带来了更复杂的部署架构。从0.7.1到0.8,全新的路由规则v1alpha3与之前的API完全不兼容,新的virtualservice与原先的routerule截然不同,给每位使用者构成了不少麻烦。

如何完全避免升级Istio给现网带来负影响,官方依然没有给出完美平滑的升级方案。此外,从0.8到1.0虽然各个组件的性能表现有显著提升,但从业内反馈来看,并没令所有人满意,Mixer的Check缓存机制究竟能多大程度缓解Policy的性能压力依然需要观察。

值得一提的是,我们发现的不少bug同时也在被社区其他开发者发现并逐一解决。令我们开心的是,UAEK团队不是信息孤岛,我们能感受到Istio官方社区正在努力高速迭代,始终在致力于解决广大开发者关心的种种问题,我们提交的issue能在数小时内被响应,这些,都让我们坚信,Istio是一个有潜力的项目,会向Kubernetes一样走向成功。

从UAEK接入用户的经验来看,用户需要正确地使用好Istio离不开前期深入的Istio文档学习。UAEK后续需致力于要简化这一过程,让用户能傻瓜化、界面化、随心所欲地定制自己的路由规则成为我们下一个愿景。

UAEK团队始终致力于改革UCloud内部研发流程,让研发提升效率,让运维不再苦恼,让所有人开心工作。除了继续完善ServiceMesh功能,下半年UAEK还会开放更多的地域和可用区,提供功能更丰富的控制台,发布自动化的代码管理打包持续集成(CI/CD)特性等等,敬请期待!

作者介绍
陈绥,UCloud资深研发工程师,先后负责监控系统、Serverless产品、PaaS平台ServiceMesh等开发,有丰富的分布式系统开发经验。

查看原文

赞 0 收藏 0 评论 0

优云数智 发布了文章 · 2018-08-20

干货分享 | 微服务配置中心架构解析

本文根据优云数智技术总监岳晓阳于8月14日可信云大会《容器和微服务》论坛演讲整理而成,主要解析了配置中心在微服务的前世今生、微服务配置中心管理及原则、微服务配置中心Hawk架构解析以及未来展望,希望对大家有所帮助。

演讲提纲:
1.微服务的前世今生;
2.微服务配置中心管理及原则;
3.微服务配置中心架构解析;
4.未来展望

一:微服务的前世今生

配置中心在整个微服务体系里算其中一小块,只是解决了分布式环境下如何去做软件配置管理的问题。首先,先简单讲解一下微服务相关概念。

微服务本身的诞生并不是一个偶然的现象,从领域驱动设计、敏捷方法论、持续交付、虚拟化和基础设施自动化、DevOps文化这些因素都是推动微服务诞生的重要因素:

  1. 领域驱动设计指导我们如何分析并模型化复杂的业务;
  2. 敏捷方法论帮助我们消除浪费,快速反馈;
  3. 持续交付促使我们构建更快、更可靠、更频繁的软件部署和交付能力;
  4. 虚拟化和基础设施自动化(Infrastructure As Code)则帮助我们简化环境的创建、安装;

5.DevOps 文化的流行以及特性团队的出现,使得小团队更加全功能化。

软件开发经历的三个阶段:单体应用、SOA架构、微服务架构。因为本人做了差不多17年的开发,亲身经历了每一个阶段,在目前这个阶段,微服务现在基本上已经被广泛接受了。

图二:单体架构Vs. 微服务架构(来源:网络)

这里做一个比较,上图中两个架构图都是从网上下载的,实际是一个订车系统的架构,图中左侧是单体架构,右侧是微服务架构,从图上可以很清晰的看到两种架构的区别。单体和微服务的优劣对比,这里就不展开讲了,总的来说各有利弊,在一定的规模下,你很难说到底哪个好、还是不好。

从DevOps的角度来看,每次构建微服务并将其部署到环境时,都要经历组装、引导、服务注册/发现、服务监控四个阶段,分别对应了微服务开发的四个原则:

➢ 微服务应该是独立的,可独立部署多个
➢ 微服务应该是可配置的
➢ 微服务实例需要对客户端透明
➢ 微服务应该传达其健康状况

二、微服务配置中心管理及原则

我们认为每一个大型的分布式微服务系统都需要一个配置中心。从单体应用那个时候的配置管理来回顾一下,左边的图描述的是按照不同的环境,提供不同的配置文件,将这些配置文件跟二进制包打包在一起,在Weblogic时代这是很流行的一种做法。

到现在,有些解决方案提供商还是用这种方式来实现,它也是容器运行环境,在打镜像的时候会把配置的信息和镜像打在一起。假设,实际当中有三个环境,前面是一个开发环境,中间是测试环境,后面是一个企业的生产环境。每个环境都有相关对应的配置文件,程序去哪个环境运行,就提供对应环境的运行包。

2.1 微服务系统到底怎么管理配置信息

一个系统到后面可能会拆成好几个微服务,每一个微服务可能有好多的实例,这些实例分布在不同的系统上,要让人手工去改这个配置系统——这些都是构建微服务系统需要考虑的问题,想想就是很可怕的事。这还只是一个系统,如果是多个系统呢?牵扯到多个数据中心呢?所以对于微服务的配置,我们有几个原则:

1.程序和配置一定要分离;
2.配置要集中进行管理;
3.同一个程序包要适应多个环境;
4.我们要提供一个客户端去拉取配置信息;
5.服务端要能够推送,这一条主要是考虑程序运行时动态修改配置的情况。

此外,还要维护多版本的配置信息、配置中心自己的容灾以及客户端规模达到一定数量的时候,必须考虑配置中心的性能问题。

2.2 微服务配置原则
Heroku创始人AdamWiggins发布了一个“十二要素应用宣言(TheTwelve-Factor App)”,为构建使用标准化流程自动配置,服务界限清晰,可移植性高,基于云计算平台可扩展的服务配置提供了方法论:

  1. 配置是可分离的,可从微服务中抽离出来,任何的配置修改不需要动一行代码;
  2. 配置应该是中央的,通过统一的中央配置平台去配置管理不同的微服务
  3. 配置中心必须必须可靠切稳定地提供配置服务;
  4. 配置是可追溯的,任何的配置历史都是可追溯,被管理且可用。

在云服务时代,对微服务做配置,对它有什么样的要求呢?首先,必须基于镜像管理部署,有自己相应独立的配置,而且程序包不可以因为环境的改变而更改。也就是说,它是独立于环境的不可变的程序包。这是我们提到的,云化微服务的配置原则:

1.完全分离要部署的程序和其对应的配置;
 2.程序包对于任何环境都是不可变的;
3.通过环境变量或配置存储在程序启动时注入配置。

2.3 配置中心功能需求分析

上图是我们对配置中心功能需求分析的整理,主要分三个大的方面:一是需要具备的功能,二是跟其它产品的集成,三是企业级的管理属性。我们认为配置中心应该具有以下四个必备要素:

1.配置数据持久化存储。
2.可以横向扩展的缓存集群。
3.配置信息的拉取和推送。
4.配置数据及配置过程管理。

三、配置中心Hawk系统架构解析

首先,接入第一层是网关,整体的存储通过Hawk Server,下发到ETCD集群,ETCD集群再同步到K8S容器运行的平台。先从数据迁移的状态简化成简单的几部分。比如新建一个配置,要么配置就被删除了,直接一步到位。如果没有这样做,就面临几种情况:

  1. 这个配置是否要小范围的去做一些试探性的发布,这种情况可以走灰度发布,状态变成灰度中,配置不允许更改。要么就是两条路走,全量发布到所有服务上。要么就是放弃灰度回到之前的状态,放弃灰度后会去到已修改的状态。
  2. 另外一种情况,新建一个配置,直接全量发布,状态变成已发布状态,这时候是可更改的。但是每一次的更改,还是会回到原来那个状态。这个更改要做灰度吗?还是做发布?还是对发布有点后悔,不打算更改了?这时,从历史版本里面找一个合适的版本,激活,然后再做一次发布,通过几个简单的回路,涵盖了大部分的业务场景。

3.1 配置数据状态变迁

Hawk Portal是主体的配置界面,用户在界面上对配置进行输入、增删、改查的管理。这些资料会有两份,一份做通过Mysql做本地存储,另一份通过Hawk Server直接同步到ETCD。

由于HAWK Server是同步到ETCD里面,也就是说ETCD相当于另外一个数据库,这当中不存在数据之间的互相抄送,从而减低丢失数据的风险。持久化,是说研发和运维在后台做数据迁移,或者数据监控时更有把握,更方便。

优云数智HAWK其实有两个ETCD,一个ETCD是做注册发现的,Hawk Server、Hawk Portal都会注册在里面,作为相关的组件。类比Spring Cloud Eureka,Eureka是注册在Eureka Server里面的一个内存列表,集群里面所有Server共享这个内存信息。这个过程优云数智做了简化,所有信息全部注册在ETCD里面。

ECTD集群由于是共享的,组件的状态和一致性得到保障。Portal和Server之间不再通过Portal注册在Server并通过心跳来维持关系而是通过共享持久化的ETCD,保证数据在任意时刻所看到的状态都是一致的,从而保证了服务的注册,以及服务发现的稳定性。

Hawk和Eureka 选择的路径不一样。Eureka是比较重量级的,HAWK则简化了这个配置,简化这种代码的复杂性,重点提高系统的完整性,打造系统闭环,通过一些相对简单的方法,提高服务的稳定性。

配置一旦通过Hawk Portal潜入本地数据库,微服务的注册服务是怎么实现配置呢?当Portal写入配置到本地数据库时,同时也会通过服务Sever去同步到ETCD,ETCD里面存储的信息,是一个持久化的数据。

通过Server实时从ETCD拉取配置,有时是运行的时候拉取,有时是启动时拉取。启动时拉取有两种策略,启动的时候拉取配置,存储到本地作为静态文件的配置,运行时候拉取,动态的变更实时生效。

在Web层其实也有一些问题需要解决,比如,因为我们不是一个开发框架,是奔着一个开源系统的方向去,所以要解决服务跟浏览器之间的授权。

优云数智现在的做法是在本土数据库存储一些用户的信息,但是并没有采用传统意义上的建Session来做验证和授权,而是通过动态下发JWT的形式,每一个请求动态下发,根据我个人用户的一些信息生成,每次的请求一来一去都有交换新的Token,每个Token实时生效并有续约的功能,来代替传统意义上的Session。

3.2 配置中心的支撑体系

第一种运维管理体系类似于偏静态类的配置,在启动时通过配置文件直接拉取读业务;另外一种是开发管理体系,偏动态管理,代表的是一种程序或者在运行过程中,通过实时的变更配置内容而实时生效,达到的一种效果。一个健全的配置中心应该支持这两种运维体系。配置中心应该具备有以下几点特性:

1.基于Spring Cloud config打造。
2.完全兼容Spring Cloud config API。
3.配置更新通过GRPC双向流实时推送。
4.采用ETCD作为配置数据的强一致性存储。
5.具备LDAP用户认证、授权管理、审批流程、审计日志等企业级特性。
6.通过Open API和查件体系扩展支持基础组件,如sharding-gdbc。

3.3 配置中心部署模式
微服务配置中心如何部署,一般来讲会是这么两种方式,生产环节中单独布一套配置中心,在开发环境部署一套配置中心,开发环境的配置中心可以支持Dev、SIT、UAT等多个环境。

四、对未来的展望

我们要做的工作,首先还是继续围绕Spring Cloud体系,为用户提供成熟方案和服务治理中心;二是探索基于Service Mesh的新方案,拥抱Istio/Conduit;三是配置中心Hawk我们已经做好了在GitHub上开源的准备工作,希望大家多多关注Hawk,关注您可信赖的云服务合作伙伴——优云数智。

图十三:未来展望

讲师介绍

岳晓阳,17年IT老兵,曾长期负责电信系统开发和互联网架构技术开发工作,具有丰富的大型软件系统技术架构和项目实施经验。对微服务、分布式系统、容器化等云计算领域的相关技术都有深入研究。

查看原文

赞 0 收藏 0 评论 0

优云数智 发布了文章 · 2018-08-01

优云数智 | Ceph 开发者月报 2018-07

UMCloud存储团队 UMCloud优云数智 昨天
图片描述

本篇为 2018 年度《Ceph 开发者月报》专栏的第七篇,在《Ceph 开发者月报》中,我们 UMCloud 存储团队将以月度为单位,为大家分享当月 Ceph 社区的有趣的提交和重要的变更,方便大家即刻掌握一手 Ceph 社区开发资料。

每篇将以对象存储、块存储、统一存储层、集群管理、基础库等模块组织,为大家一一介绍。

本月看点:

Ceph 对象存储认证支持 CNCF项目 OPA 实现细粒度的访问控制

对象存储

完善 radosgw-admin sync error trim 命令

radosgw-admin: ‘sync error trim’ loops until complete

( https://github.com/ceph/ceph/... )

之前

radosgw-admin sync error trim

命令最多只能清理 1000 条记录,即使通过 start_time/end_time 或是 start_marker/end_marker 指定了清理范围,但仍最多只能清理 1000 条记录,
这就意味着若指定范围内的 sync error 日志记录多于 1000 条,则该命令只能处理一部分日志记录。

在上面的提交中,社区对该命令进行了完善,支持清理指定范围内的所有错误日志记录。同时,还新增了 –trim-delay-ms 参数,用于控制清理操作的执行频率。

Ceph RGW 集成 OPA 策略引擎

rgw: Initial work for OPA-Ceph integration

( https://github.com/ceph/ceph/... )

OPA 是一个轻量级的开源通用策略引擎,可以在整个项目开发堆栈中实现统一的、上下文感知的策略实施。
当前社区希望在 RGW 中集成 OPA。在上面的提交中,社区为 RGW 和 OPA 的集成进行了一部分初期工作。

块存储

librbd 支持 FUA

librbd:optionally support FUA (force unit access) on write requests

( https://github.com/ceph/ceph/... )

在上面的提交中,社区为 librbd 新增了对 FUA 的支持。若设置为 FUA 模式,对于写操作请求,必须将数据写入到存储卷后,才返回成功信息。所有的写请求处理都完全跳过缓存。

统一存储层

mon 新增 pg repeer <pgid> 命令

mon/OSDMonitor: add ‘osd repeer <pgid>’ command

( https://github.com/ceph/ceph/... )

在上面的提交中,社区为 mon 新增了

pg repeer <pgid>

命令,用于强制 pgid 参数指定的 PG 执行 peer 处理。

集群管理

mgr 新增 crash 插件

mgr/pybind/crash: handle crashdumps

( https://github.com/ceph/ceph/... )

在上面的提交中,社区为 mgr 实现了 crash 插件,用于收集集群中各组件的 crash dump 信息,并存储在 Ceph 集群中,方便日后进行分析。

针对 crash 插件,主要新增了如下命令

启用 crash 插件

ceph mgr module enable crash

保存一个 crash dump 信息

ceph crash post -i <metafile>

移除一个指定的 crash dump 信息

ceph rm <crashid>

罗列出保存的所有 crash dump 信息

ceph crash ls

对所保存的 crash dump 信息进行统计和总结

ceph crash stat

获取所保存的指定 crash 过程的具体细节信息

ceph crash info <crashid>

移除所有保存时间大于 keep 字段所指定天数的 crash dump 信息

ceph crash prune <keep>

实现 mgr 内部各模块之间可以相互调用

mgr: enable inter-module calls

( https://github.com/ceph/ceph/... )

dashboard 新增用户管理界面

mgr/dashboard: Ceph dashboard user management from the UI

( https://github.com/ceph/ceph/... )

工具库

ceph-volume 新增批量操作命令及相关的操作处理

ceph-volume batch command

( https://github.com/ceph/ceph/... )

vstart.sh 支持启用 SPDK

vstart.sh: Support SPDK in Ceph development deployment

( https://github.com/ceph/ceph/... )

本月提交情况

图片描述

查看原文

赞 0 收藏 0 评论 0

优云数智 发布了文章 · 2018-07-26

东湖行 | 优云数智UMStor展示PB级数据湖存储方案实力

在近日举行的2018全球闪存技术峰会上,优云数智携旗下具有“CBA”气息的存储系统——UMStor来到武汉东湖之滨的光谷科技会展中心,向参会者展示PB级数据湖存储方案实力。

数据流冲击日益凸显

100多年前,“千湖之地”武汉湖泊星罗棋布,沙湖、东湖、白洋湖相通,因无人建防水堤,每每水患,所望及尽泽国。光绪二十五年,湖广总督张之洞在长江与东湖之间修建了南北两段堤防——武金堤和武青堤,至此东湖与沙湖分离,基于坚固地质,面积经几十年不变,定于33平方公里,遂成今东湖。

探究东湖发展,人为分治起到很大作用。然而,今天人们开始承受另一种“水患之痛”,那就是高速发展的IoT使大数据面临PB级海量数据存储以及上千QPS级并发请求,大数据流正疯狂冲击着企业。

面对全业务数字化转型大势,传统企业数据中心的数据存储级别由原先数十TB向今天动辄PB级的数据存储量级转化。与此同时,围绕企业多类型业务需求,如何解决海量大数据并发承载,如何结合AI对存储大数据挖掘,并同步通过多协议访问高效对外分享等一系列问题,就需建立统一的数据平台化运营机制。

在数字化转型的挑战面前,各种新型技术渗透业务模式和流程,推动传统企业数据存储变革。UMStor不仅协助企业大数据上云端,支持大数据加速,还为人工智能提供快速处理数据接口。

至此,优云数智UMStor已拥有诸多数十PB级的统一分布存储、大数据湖实际案例落地。

PB级数据湖存储方案

数据湖的核心关键依然在于数字化转型,包含大数据存储、处理、共享等数据平台化运营机制。优云数智UMStor多协议分布式存储系统在诸多实际案例落地过程中,演化出一种新的大数据存储架构——数据湖存储架构。

(图:UMStor PB级数据湖存储方案示例)

结合其他软件应用服务启示,优云数智UMStor通过将计算存储环境分离,采用Hadapter直接特定函数调用,完成数据访问,避免增加网关IO访问瓶颈,在保证大数据环境下的存储访问性能同时,打破环境分离带来的数据调度壁垒。

UMStor协助企业存储数据上云端

传统大数据的物理机部署模式在应对当下企业多业务弹性计算资源需求,以及现有存储容量扩容增长相差较大的情况下,计算、存储两者一体部署已无法满足灵活业务需求,同时也并非是最优性价比方案。而将存储数据与计算分离,独立上云端,实现两者各自独立扩展,正是当下数据存储的趋势。

UMStor作为企业数据上云端方案,是真正的软件定义存储系统(SDS),采用领先的全分布式全冗余架构,没有单点故障,具有高弹性和高可靠性,性能和容量可以横向扩展。UMStor可以灵活进行软件配置与硬件选型,协助企业自定义存储系统的性能、容量、数据保护能力等,在基础资源利用最大化的同时,也降低基础成本投入,满足企业当前和未来数据存储战略需求。

UMStor助力企业大数据加速

伴随数字化发展,传统数据存储部分问题日益突出。例如,传统存储数据的存取路径须通过对象服务的网关,再由网关服务把IO请求递交给底层的对象存储设备,这就会导致一系列问题,不仅网关增加了IO访问路径的开销,而且该网关比较容易成为系统的性能瓶颈。

(图:Hadapter运行机制)

通过NFS-Ganesha软件支持多种后台存储系统中Ceph对象存储服务的启示,优云数智打造了一款基于自身存储系统的Hadoop插件——Hadapter。UMStor通过将计算存储环境分离,采用Hadapter直接调用librados函数库来请求OSD的方式,避开了数据存取对网关的请求,在保证大数据环境下存储访问性能的同时,打破环境分离带来的数据调度壁垒。

此外,围绕大数据应用对接,UMStor不仅提供标准S3和Swift对象存储接口,用于存储图片、文件、视频等非结构化数据,而且还提供HDFS接口,大数据应用Hadoop、Spark、Hive、HBase可以直接与存储服务器进行通信,不需要经过存储网关和元数据服务器,具有很强的横向扩展能力和吞吐能力。

UMStor推动人工智能AI大数据挖掘

大数据时代,人工智能同步飞速发展。智能算法的训练与推理需要对超大数据集进行处理与分析,而这些数据涵盖视频、图片、文本等非结构化数据。要对来源不同行业、组织、部门、项目的数据进行采集、存储、清洗、转换、特征提取等工作,无疑是一项复杂、漫长的工程。

UMStor不仅可向人工智能程序提供数据快速收集、处理、分析平台,还具备提供高速

带宽、海量小文件存取、多协议互通、数据共享的能力,从而大大加速了数据挖掘、深度学习的过程。

曾经水患肆虐的东湖,如今已成为武汉存储产业高科技发展中心,未来将影响着中国,乃至世界科技产业的发展方向。UMStor作为优云数智推出的具“CBA”气息的存储系统,也将为传统企业的数字化转型提供便捷、稳定的数据云端存储解决方案。

查看原文

赞 0 收藏 0 评论 0

认证与成就

  • 获得 227 次点赞
  • 获得 2 枚徽章 获得 0 枚金徽章, 获得 0 枚银徽章, 获得 2 枚铜徽章

擅长技能
编辑

(゚∀゚ )
暂时没有

开源项目 & 著作
编辑

(゚∀゚ )
暂时没有

注册于 2015-12-14
个人主页被 2.3k 人浏览