nova boot代码流程分析(五):VM启动从neutron-dhcp-agent获取IP与MAC

来源:互联网 发布:js 汉字长度 编辑:程序博客网 时间:2024/05/22 06:33

1.   network和subnet创建代码流程

[root@jun ~(keystone_user1)]# neutron net-create demo-net

[root@jun ~(keystone_user1)]# neutron subnet-create  demo-net 1.1.1.0/24 --name demo-subnet --gateway 1.1.1.1 --enable_dhcp true

这里,我们主要分析上面两个命令的代码流程,我们关注的重点是neutron-server如何下发的命令到neutron-dhcp-agent去创建相应的network的代码流程。

首先neutronclient发送HTTP请求给neutron-server,去执行下面的create函数。

#/neutron/api/v2/base.py:Controller    def create(self, request, body=None, **kwargs):        """Creates a new instance of the requested entity."""        parent_id = kwargs.get(self._parent_id_name)        self._notifier.info(request.context,                            self._resource + '.create.start',                            body)        body = Controller.prepare_request_body(request.context, body, True,                                               self._resource, self._attr_info,                                               allow_bulk=self._allow_bulk)        action = self._plugin_handlers[self.CREATE]        ... ... ...        def notify(create_result):            notifier_method = self._resource + '.create.end'            self._notifier.info(request.context,                                notifier_method,                                create_result)            self._send_dhcp_notification(request.context,                                         create_result,                                         notifier_method)            return create_result        kwargs = {self._parent_id_name: parent_id} if parent_id else {}        if self._collection in body and self._native_bulk:            # plugin does atomic bulk create operations            obj_creator = getattr(self._plugin, "%s_bulk" % action)            objs = obj_creator(request.context, body, **kwargs)            # Use first element of list to discriminate attributes which            # should be removed because of authZ policies            fields_to_strip = self._exclude_attributes_by_policy(                request.context, objs[0])            return notify({self._collection: [self._filter_attributes(                request.context, obj, fields_to_strip=fields_to_strip)                for obj in objs]})        else:            obj_creator = getattr(self._plugin, action)            if self._collection in body:                # Emulate atomic bulk behavior                objs = self._emulate_bulk_create(obj_creator, request,                                                 body, parent_id)                return notify({self._collection: objs})            else:                kwargs.update({self._resource: body})                obj = obj_creator(request.context, **kwargs)                self._send_nova_notification(action, {},                                             {self._resource: obj})                return notify({self._resource: self._view(request.context,                                                          obj)})

这个create函数实现的功能为:

1. 通过调用core_plugin的core resource所对应的action方法(如create_network)在数据库中保存resource信息。

2. 在保存自身的信息到数据库中后,通知agent去做相应的操作。

其实对于port,network,subnet的create操作都会走到这个create函数。这里我们主要分析创建network(包括subnet)时与neutron-dhcp-agent之间的操作。

在network(或subnet)的信息保存到数据库之后,通过notify函数(create函数内部的函数)通知neutron-dhcp-agent做相应的操作。

network和subnet发送的给neutron-dhcp-agent的方法(notifier_method)分别为network.create.endsubnet.create.end

#/neutron/api/v2/base.py:Controller    def _send_dhcp_notification(self, context, data, methodname):        if cfg.CONF.dhcp_agent_notification:            if self._collection in data:                for body in data[self._collection]:                    item = {self._resource: body}                    self._dhcp_agent_notifier.notify(context, item, methodname)            else:                self._dhcp_agent_notifier.notify(context, data, methodname)

dhcp_agent_notification为/etc/neuton/neutron.conf配置文件中的参数。

# Allow sending resource operation notification to DHCP agent

# dhcp_agent_notification = True

dhcp_agent_notification = True

 

#/neutron/api/rpc/agentnotifiers/dhcp_rpc_agent_api.py:DhcpAgentNotifyAPI    def notify(self, context, data, method_name):        # data is {'key' : 'value'} with only one key        if method_name not in self.VALID_METHOD_NAMES:            return        obj_type = data.keys()[0]        if obj_type not in self.VALID_RESOURCES:            return        obj_value = data[obj_type]        network_id = None        if obj_type == 'network' and 'id' in obj_value:            network_id = obj_value['id']        elif obj_type in ['port', 'subnet'] and 'network_id' in obj_value:            network_id = obj_value['network_id']        if not network_id:            return        method_name = method_name.replace(".", "_")        if method_name.endswith("_delete_end"):            if 'id' in obj_value:                self._notify_agents(context, method_name,                                    {obj_type + '_id': obj_value['id']},                                    network_id)        else:            self._notify_agents(context, method_name, data, network_id)#/neutron/api/rpc/agentnotifiers/dhcp_rpc_agent_api.py:DhcpAgentNotifyAPI    def _notify_agents(self, context, method, payload, network_id):        """Notify all the agents that are hosting the network."""        # fanout is required as we do not know who is "listening"        no_agents = not utils.is_extension_supported(            self.plugin, constants.DHCP_AGENT_SCHEDULER_EXT_ALIAS)        fanout_required = method == 'network_delete_end' or no_agents        # we do nothing on network creation because we want to give the        # admin the chance to associate an agent to the network manually        cast_required = method != 'network_create_end'        if fanout_required:            self._fanout_message(context, method, payload)        elif cast_required:            admin_ctx = (context if context.is_admin else context.elevated())            network = self.plugin.get_network(admin_ctx, network_id)            agents = self.plugin.get_dhcp_agents_hosting_networks(                context, [network_id])            # schedule the network first, if needed            schedule_required = (                method == 'subnet_create_end' or                method == 'port_create_end' and                not self._is_reserved_dhcp_port(payload['port']))            if schedule_required:                agents = self._schedule_network(admin_ctx, network, agents)            enabled_agents = self._get_enabled_agents(                context, network, agents, method, payload)            for agent in enabled_agents:                self._cast_message(                    context, method, payload, agent.host, agent.topic)

在_notify_agents函数可以看出,由于创建network时,fanout_required和cast_required的值为False,所以创建network时,并不会通过neutron-dhcp-agent做什么操作。而创建port或subnet时,则会通过rpc方式执行neutron-dhcp-agent的方法,从而做一些操作。所以下面我们主要分析在创建subnet时,neutron-dhcp-agent做了哪些操作。

#/neutron/api/rpc/agentnotifiers/dhcp_rpc_agent_api.py:DhcpAgentNotifyAPI    def _cast_message(self, context, method, payload, host,                      topic=topics.DHCP_AGENT):        """Cast the payload to the dhcp agent running on the host."""        cctxt = self.client.prepare(topic=topic, server=host)        cctxt.cast(context, method, payload=payload)#/neutron/agent/dhcp/agent.py:DhcpAgent    @utils.synchronized('dhcp-agent')    def subnet_update_end(self, context, payload):        """Handle the subnet.update.end notification event."""        network_id = payload['subnet']['network_id']        self.refresh_dhcp_helper(network_id)    # Use the update handler for the subnet create event.    subnet_create_end = subnet_update_end

看到/neutron/agent/dhcp/agent.py:DhcpAgent类是不是很熟悉,这正是上一篇文章中分析的在neutron-dhcp-agent服务启动时,创建的DhcpAgent对象。

#/neutron/agent/dhcp/agent.py:DhcpAgent    def refresh_dhcp_helper(self, network_id):        """Refresh or disable DHCP for a network depending on the current state        of the network.        """        old_network = self.cache.get_network_by_id(network_id)        if not old_network:            # DHCP current not running for network.            return self.enable_dhcp_helper(network_id)        network = self.safe_get_network_info(network_id)        if not network:            return        old_cidrs = set(s.cidr for s in old_network.subnets if s.enable_dhcp)        new_cidrs = set(s.cidr for s in network.subnets if s.enable_dhcp)        if new_cidrs and old_cidrs == new_cidrs:            self.call_driver('reload_allocations', network)            self.cache.put(network)        elif new_cidrs:            if self.call_driver('restart', network):                self.cache.put(network)        else:            self.disable_dhcp_helper(network.id)

refresh_dhcp_helper函数将对比self.cache中存放的network信息与新创建的network信息,通过对比结果做相应操作。

1. 如果self.cache中没有新创建的network信息,则调用enable_dhcp_helper函数,该函数最终调用driver(dhcp_driver = neutron.agent.linux.dhcp.Dnsmasq)的enable函数。

2. 如果self.cache中有新创建的network信息,则判断old network与new network的cidr是否相等,根据结果判断调用driver中的reload_allocations函数还是restart函数,又或者是disable函数。

对于执行neutron.agent.linux.dhcp.Dnsmasq driver中的相关函数(enable, reload_allocations, restart,disable),我们在这里本篇文章就不分析了,具体查看《neutron-dhcp-agent服务启动流程》。

PS:在《neutron-dhcp-agent服务启动流程》文章中,我们也有分析,如果创建的subnet并没有enable dhcp,则neutron-dhcp-agent不会为该network创建namespace,创建device以及创建dnsmasq进程。具体代码如下。

#/neutron/agent/dhcp/agent.py:DhcpAgent    def configure_dhcp_for_network(self, network):        if not network.admin_state_up:            return        enable_metadata = self.dhcp_driver_cls.should_enable_metadata(                self.conf, network)        dhcp_network_enabled = False        for subnet in network.subnets:            if subnet.enable_dhcp:                if self.call_driver('enable', network):                    dhcp_network_enabled = True                    self.cache.put(network)                break        if enable_metadata and dhcp_network_enabled:            for subnet in network.subnets:                if subnet.ip_version == 4 and subnet.enable_dhcp:                    self.enable_isolated_metadata_proxy(network)                    break

小结一下:

1. 创建network,subnet和port时,首先调用core_plugin提供的函数向数据库写入自身的信息。

2. 写完数据库之后,subnet和port通过rpc方式,执行neutron-dhcp-agent启动时创建的DhcpAgent对象提供的函数(这里创建network时,并不会执行neutron-dhcp-agent所提供的函数方法)。

3. neutron-dhcp-agent对创建subnet的操作可以参考《neutron-dhcp-agent服务启动流程》文章。

对于neutron-dhcp-agent对创建VM时所创建的port的操作我们在下一小节分析。

2. VM启动时从neutron-dhcp-agent获取IP与MAC的代码流程分析

2.1 更新dnsmasq配置文件信息

接到《nova boot代码流程分析()novaneutronplugin交互》的创建port的代码流程分析,因为在那篇文章中,主要关注的如何将创建的IP和MAC保存到数据库,并未关注VM实际如何从neutron-dhcp-agent获取IP和MAC的代码流程,所以在这篇文章中,我们将关注VM实际获取IP和MAC的代码流程。

#/nova/network/neutronv2/api.py:API    def _create_port(self, port_client, instance, network_id, port_req_body,                     fixed_ip=None, security_group_ids=None,                     available_macs=None, dhcp_opts=None):        """Attempts to create a port for the instance on the given network.        :param port_client: The client to use to create the port.        :param instance: Create the port for the given instance.        :param network_id: Create the port on the given network.        :param port_req_body: Pre-populated port request. Should have the            device_id, device_owner, and any required neutron extension values.        :param fixed_ip: Optional fixed IP to use from the given network.        :param security_group_ids: Optional list of security group IDs to            apply to the port.        :param available_macs: Optional set of available MAC addresses,            from which one will be used at random.        :param dhcp_opts: Optional DHCP options.        :returns: ID of the created port.        :raises PortLimitExceeded: If neutron fails with an OverQuota error.        :raises NoMoreFixedIps: If neutron fails with            IpAddressGenerationFailure error.        """        try:            if fixed_ip:                port_req_body['port']['fixed_ips'] = [                    {'ip_address': str(fixed_ip)}]            port_req_body['port']['network_id'] = network_id            port_req_body['port']['admin_state_up'] = True            port_req_body['port']['tenant_id'] = instance.project_id            if security_group_ids:                port_req_body['port']['security_groups'] = security_group_ids            if available_macs is not None:                if not available_macs:                    raise exception.PortNotFree(                        instance=instance.uuid)                mac_address = available_macs.pop()                port_req_body['port']['mac_address'] = mac_address            if dhcp_opts is not None:                port_req_body['port']['extra_dhcp_opts'] = dhcp_opts            port_id = port_client.create_port(port_req_body)['port']['id']            LOG.debug('Successfully created port: %s', port_id,                      instance=instance)            return port_id        except neutron_client_exc.IpAddressInUseClient:            LOG.warning(_LW('Neutron error: Fixed IP %s is '                            'already in use.'), fixed_ip)            msg = _("Fixed IP %s is already in use.") % fixed_ip            raise exception.FixedIpAlreadyInUse(message=msg)        except neutron_client_exc.OverQuotaClient:            LOG.warning(_LW(                'Neutron error: Port quota exceeded in tenant: %s'),                port_req_body['port']['tenant_id'], instance=instance)            raise exception.PortLimitExceeded()        except neutron_client_exc.IpAddressGenerationFailureClient:            LOG.warning(_LW('Neutron error: No more fixed IPs in network: %s'),                        network_id, instance=instance)            raise exception.NoMoreFixedIps(net=network_id)        except neutron_client_exc.MacAddressInUseClient:            LOG.warning(_LW('Neutron error: MAC address %(mac)s is already '                            'in use on network %(network)s.') %                        {'mac': mac_address, 'network': network_id},                        instance=instance)            raise exception.PortInUse(port_id=mac_address)        except neutron_client_exc.NeutronClientException:            with excutils.save_and_reraise_exception():                LOG.exception(_LE('Neutron error creating port on network %s'),                              network_id, instance=instance)

这部分代码是从《nova boot代码流程分析()novaneutronplugin交互》文章中提取出来的代码,该代码是nova-compute通过HTTP请求在neutron中创建port信息。然后到达我们第一小节分析的create函数。

#/neutron/api/v2/base.py:Controller    def create(self, request, body=None, **kwargs):        """Creates a new instance of the requested entity."""        parent_id = kwargs.get(self._parent_id_name)        self._notifier.info(request.context,                            self._resource + '.create.start',                            body)        body = Controller.prepare_request_body(request.context, body, True,                                               self._resource, self._attr_info,                                               allow_bulk=self._allow_bulk)        action = self._plugin_handlers[self.CREATE]        ... ... ...        def notify(create_result):            notifier_method = self._resource + '.create.end'            self._notifier.info(request.context,                                notifier_method,                                create_result)            self._send_dhcp_notification(request.context,                                         create_result,                                         notifier_method)            return create_result        kwargs = {self._parent_id_name: parent_id} if parent_id else {}        if self._collection in body and self._native_bulk:            # plugin does atomic bulk create operations            obj_creator = getattr(self._plugin, "%s_bulk" % action)            objs = obj_creator(request.context, body, **kwargs)            # Use first element of list to discriminate attributes which            # should be removed because of authZ policies            fields_to_strip = self._exclude_attributes_by_policy(                request.context, objs[0])            return notify({self._collection: [self._filter_attributes(                request.context, obj, fields_to_strip=fields_to_strip)                for obj in objs]})        else:            obj_creator = getattr(self._plugin, action)            if self._collection in body:                # Emulate atomic bulk behavior                objs = self._emulate_bulk_create(obj_creator, request,                                                 body, parent_id)                return notify({self._collection: objs})            else:                kwargs.update({self._resource: body})                obj = obj_creator(request.context, **kwargs)                self._send_nova_notification(action, {},                                             {self._resource: obj})                return notify({self._resource: self._view(request.context,                                                          obj)})

create函数首先创建IP和MAC信息保存到neutron数据库中,具体参考《nova boot代码流程分析()novaneutronplugin交互》文章。然后调用notify函数发送port.create.end给neutron-dhcp-agent。最终neutron-dhcp-agent将调用port_create_end函数。

#/neutron/agent/dhcp/agent.py:DhcpAgent    @utils.synchronized('dhcp-agent')    def port_update_end(self, context, payload):        """Handle the port.update.end notification event."""        updated_port = dhcp.DictModel(payload['port'])        network = self.cache.get_network_by_id(updated_port.network_id)        if network:            driver_action = 'reload_allocations'            if self._is_port_on_this_agent(updated_port):                orig = self.cache.get_port_by_id(updated_port['id'])                # assume IP change if not in cache                old_ips = {i['ip_address'] for i in orig['fixed_ips'] or []}                new_ips = {i['ip_address'] for i in updated_port['fixed_ips']}                if old_ips != new_ips:                    driver_action = 'restart'            self.cache.put_port(updated_port)            self.call_driver(driver_action, network)    def _is_port_on_this_agent(self, port):        thishost = utils.get_dhcp_agent_device_id(            port['network_id'], self.conf.host)        return port['device_id'] == thishost    # Use the update handler for the port create event.    port_create_end = port_update_end

具体如何到达该代码流程,可以参考第1小节创建subnet的代码流程。这里将port_update_end函数赋给port_create_end函数,所以最终是调用port_update_end函数。port_update_end函数将比较self.cache中保存的ip信息与最新数据库中相对应的network中对应的ip信息(从neutron-server传递下来的,即payload)。正常流程下,因为我们创建VM会创建新的port信息到数据库中,而neutron-dhcp-agent的self.cache中还保存着上一次更新的network信息,所以导致数据库中port信息与self.cache中保存的port信息不一致,因此将更新self.cache中的port信息,且执行reload_allocations函数,重新更新dnsmasq的配置文件。

#/neutron/agent/linux/dhcp.py:Dnsmasq    def reload_allocations(self):        """Rebuild the dnsmasq config and signal the dnsmasq to reload."""        # If all subnets turn off dhcp, kill the process.        if not self._enable_dhcp():            self.disable()            LOG.debug('Killing dnsmasq for network since all subnets have '                      'turned off DHCP: %s', self.network.id)            return        self._release_unused_leases()        self._spawn_or_reload_process(reload_with_HUP=True)        LOG.debug('Reloading allocations for network: %s', self.network.id)        self.device_manager.update(self.network, self.interface_name)

调用_release_unused_leases函数release在leases文件(/var/lib/neutron/dhcp/43c0e274-28e3-482e-a32b-d783980fc3ed/leases)中未被使用的ip和mac。然后再重新加载dnsmasq进程所需的配置文件,由于加载所需的配置文件我们在《neutron-dhcp-agent服务启动流程》中有分析,所以我们这里主要分析release在leases文件中未被使用的ip和mac。

#/neutron/agent/dhcp/agent.py:DhcpAgent    def _release_unused_leases(self):        filename = self.get_conf_file_name('host')        old_leases = self._read_hosts_file_leases(filename)        new_leases = set()        for port in self.network.ports:            for alloc in port.fixed_ips:                new_leases.add((alloc.ip_address, port.mac_address))        for ip, mac in old_leases - new_leases:            self._release_lease(mac, ip)#/neutron/agent/dhcp/agent.py:DhcpAgent    def _release_lease(self, mac_address, ip):        """Release a DHCP lease."""        cmd = ['dhcp_release', self.interface_name, ip, mac_address]        ip_wrapper = ip_lib.IPWrapper(namespace=self.network.namespace)        ip_wrapper.netns.execute(cmd, run_as_root=True)

_release_unused_leases函数读取host文件(/var/lib/neutron/dhcp/43c0e274-28e3-482e-a32b-d783980fc3ed/host)中的被dnsmasq分配的ip和mac信息,将这些信息与数据库中network的ip和mac信息作对比,如果dnsmasq分配的ip和mac信息并未在数据库中,说明此ip和mac信息未被使用,所以调用_release_lease函数对其进行释放。

_release_lease函数是通过dhcp_release命令向dnsmasq进程监听接口ns-xxx发送DHCPRELEASE请求来释放未被使用的ip和mac信息。即删除leases文件未被使用的ip和mac信息。下面是一个测试用例。

开始leases文件信息:

[root@nova 43c0e274-28e3-482e-a32b-d783980fc3ed]# cat leases

1464597255 fa:16:3e:da:42:50 1.1.1.2 host-1-1-1-2 *

1464597255 fa:16:3e:d0:eb:87 1.1.1.9 host-1-1-1-9 *

1464597255 fa:16:3e:d1:d7:72 1.1.1.1 host-1-1-1-1 *

假设我们将release红色部分的ip和mac信息,则执行以下操作。

[root@nova ~]# ip netns exec qdhcp-43c0e274-28e3-482e-a32b-d783980fc3ed dhcp_release ns-f7620da4-39 1.1.1.9 fa:16:3e:d0:eb:87

在/var/log/messages文件中,可以看到如下日志。

May 29 04:38:09 nova dnsmasq-dhcp[3759]: DHCPRELEASE(ns-f7620da4-39) 1.1.1.9 fa:16:3e:d0:eb:87

执行dhcp_release命令后的leases文件信息:

[root@nova 43c0e274-28e3-482e-a32b-d783980fc3ed]# cat leases

1464597255 fa:16:3e:da:42:50 1.1.1.2 host-1-1-1-2 *

1464597255 fa:16:3e:d1:d7:72 1.1.1.1 host-1-1-1-1 *

可以看出,ip为1.1.1.9相关的信息被删除了。

这是_release_unused_leases函数的功能,而reload_allocations函数剩下的代码则是重新配置dnsmasq的配置文件,假设我们创建VM在数据库中分配的ip为1.1.1.10,那么在host文件中将把该ip及相对应的mac写入进去,待VM启动且发送dhcp discover请求时将该IP和mac分配于它。

2.2 分配ip和mac给VM

在创建VM开始时,首先为VM在数据库中创建port信息,创建port信息完成后,通知neutron-dhcp-agent更新dnsmasq的配置文件(包括host和addn_hosts文件)信息。如下

[root@nova 43c0e274-28e3-482e-a32b-d783980fc3ed]# cat addn_hosts

1.1.1.1 host-1-1-1-1.openstacklocal host-1-1-1-1

1.1.1.2 host-1-1-1-2.openstacklocal host-1-1-1-2

1.1.1.10        host-1-1-1-10.openstacklocal host-1-1-1-10

 [root@nova 43c0e274-28e3-482e-a32b-d783980fc3ed]# cat host

fa:16:3e:d1:d7:72,host-1-1-1-1.openstacklocal,1.1.1.1

fa:16:3e:da:42:50,host-1-1-1-2.openstacklocal,1.1.1.2

fa:16:3e:3c:a3:3e,host-1-1-1-10.openstacklocal,1.1.1.10

这是在VM启动之前,就已经保存到dnsmasq的配置文件中的信息。

等待VM启动时,发送dhcp discover请求。

VM启动的有关dhcp的log如下(这是dhcp client端)。

Starting network...

udhcpc (v1.20.1) started

Sending discover...

Sending select for 1.1.1.10...

Lease of 1.1.1.10 obtained, lease time 86400

cirros-ds 'net' up at 6.03

/var/log/messages的有关dhcp的log如下(这是dhcp server端)。

May 29 05:05:34 nova dnsmasq-dhcp[4388]: DHCPDISCOVER(ns-f7620da4-39) fa:16:3e:3c:a3:3e

May 29 05:05:34 nova dnsmasq-dhcp[4388]: DHCPOFFER(ns-f7620da4-39) 1.1.1.10 fa:16:3e:3c:a3:3e

May 29 05:05:34 nova dnsmasq-dhcp[4388]: DHCPREQUEST(ns-f7620da4-39) 1.1.1.10 fa:16:3e:3c:a3:3e

May 29 05:05:34 nova dnsmasq-dhcp[4388]: DHCPACK(ns-f7620da4-39) 1.1.1.10 fa:16:3e:3c:a3:3e host-1-1-1-10

同时neutron-dhcp-agent更新dnsmasq的leases文件信息。

[root@nova 43c0e274-28e3-482e-a32b-d783980fc3ed]# cat leases

1464599134 fa:16:3e:3c:a3:3e 1.1.1.10 host-1-1-1-10 01:fa:16:3e:3c:a3:3e

1464598886 fa:16:3e:da:42:50 1.1.1.2 host-1-1-1-2 *

1464598886 fa:16:3e:d1:d7:72 1.1.1.1 host-1-1-1-1 *

Dhcp client与dhcp server的交互过程如下图所示。



这里创建VM时,从neutron-dhcp-agent获取ip和mac的代码流程便分析完成。总结一下:

1. 创建VM时,nova-compute与neutron的plugin交互,在neutron的数据库中创建VM所需的port信息。

2. neutron数据库中的port信息创建完成后,通知neutron-dhcp-agent去执行port_create_end函数。该函数将数据库中的port中的ip和mac信息加载到dnsmasq所需的配置文件中(包括host和addn_hosts文件)。

3. 在VM启动时,广播dhcp discover请求,当dnsmasq进程的监听接口ns-xxx监听到这种请求时,dnsmasq进程将根据配置文件(host和leases文件)中的内容去判定是否有未分配的ip和mac为请求者进行提供。

4. 最终VM便真实的获取到与保存在数据库中的ip和mac信息。neutron-dhcp-agent只是将所创建VM的ip和mac信息从数据库中获取到自己的配置文件中,然后等到VM启动时,为它提供。因此neutron-dhcp-agent相当于在VM和数据库之间起了个中间桥梁的作用。

2 0
原创粉丝点击