主要内容

本页采用了机器翻译。点击此处可查看最新英文版本。

管理已部署存档中的应用程序状态

此示例说明如何管理部署到 MATLAB® Production Server™ 应用程序存档中的持久数据。它使用 MATLAB Production Server用于 MATLAB 函数执行的 RESTful API 和 JSON 将 MATLAB 应用程序的一个或多个实例连接到部署在服务器上的存档。

MATLAB Production Server 工作进程是无状态的。持久性通过在服务器上部署的多次调用 MATLAB 代码之间缓存数据来提供一种维持状态的机制。多个工作进程可以访问缓存的数据。

该示例描述了两个工作流。

  1. 在将应用程序部署到服务器之前,在 MATLAB 桌面环境中测试应用程序功能的测试工作工作流。

  2. 使用活动的 MATLAB Production Server 实例来部署存档的部署工作流。

为了演示如何使用持久性,此示例使用了旅行商问题,其中涉及寻找城市之间最短的路由。此实现将持久的 MATLABgraph (MATLAB) 对象存储在数据缓存中。城市构成图的节点,城市之间的距离构成与图边相关的权重。在这个示例中,该图是一个完全图。测试工作流使用本地版本的路由查找功能。部署工作流使用打包到存档中并部署到服务器的路由查找功能。MATLAB 应用程序调用路由查找函数。这些函数从缓存中读取图形数据并将图形数据写入缓存。

示例的代码位于 $MPS_INSTALL/client/matlab/examples/persistence/TravelingSalesman,其中 $MPS_INSTALL 是安装 MATLAB Production Server 的位置。

要托管使用 Production Server Compiler 创建的可部署存档,您必须安装与用于创建存档的 MATLAB 版本兼容的 MATLAB Runtime 版本。有关详细信息,请参阅MATLAB Production Server 支持的 MATLAB Runtime 版本

步骤 1:编写使用持久性函数的 MATLAB 代码

  1. 编写函数来初始化持久数据

    编写一个函数来检查数据缓存中是否存在城市和距离的图形。如果图形不存在,则从包含距离数据的 Excel® 电子表格创建它并将其写入缓存。由于每次只有一个 MATLAB Production Server 工作进程可以执行此写入操作,因此使用同步锁来确保数据初始化只发生一次。

    使用 mps.cache.connect 连接到存储距离数据的缓存,如果不存在则创建它。在写入操作期间,使用 mps.sync.mutex 获取互斥锁。一旦数据写入缓存,就释放锁。

    使用 loadDistanceData 函数初始化距离数据。

    function tf = loadDistanceData(connectionName, cacheName)
        c = mps.cache.connect(cacheName,'Connection',connectionName);    
        tries = 0;
    
        while isKey(c,'Distances') == false && tries < 6
            lk = mps.sync.mutex('DistanceData','Connection',connectionName);       
            if acquire(lk,10)
                if isKey(c,'Distances') == false
                    g = initDistanceData('Distances.xlsx');
                    c.Distances = g;
                end
                release(lk);
            end
            tries = tries + 1;
        end   
        tf = isKey(c,'Distances');
    end
  2. 编写函数来读取持久数据

    编写一个函数从数据缓存中读取距离数据图。因为从缓存中读取数据是幂等操作,所以不需要使用同步锁。使用 mps.cache.connect 连接到缓存,然后检索图形。

    从缓存中读取图形并使用 listDestinations 函数将其转换为元胞数组。

    使用 findRoute 函数计算最短路由。使用最近邻算法,从给定城市开始并反复访问下一个最近的城市,直到访问过所有城市。

    function destinations = listDestinations()
        c = mps.cache.connect('TravelingSalesman','Connection','ScratchPad');   
        if loadDistanceData('ScratchPad','TravelingSalesman') == false
            error('Failed to load distance data. Cannot continue.');
        end
        
        g = c.Distances;
        destinations = table2array(g.Nodes);
    end
    function [route,distance] = findRoute(start,destinations)
        c = mps.cache.connect('TravelingSalesman','Connection','ScratchPad');    
        if loadDistanceData('ScratchPad','TravelingSalesman') == false
            error('Failed to load distance data. Cannot continue.');
        end
            
        g = c.Distances;    
        route = {start};
        distance = 0;
        current = start;
        
        while ~isempty(destinations)
            minDistance = Inf;
            nextSegment = {};        
            for n = 1:numel(destinations)
                [p,d] = shortestpath(g,current,destinations{n});
                if d < minDistance
                    nextSegment = p(2:end);
                    minDistance = d;
                end
            end
    
            current = nextSegment{end};
            distance = distance + minDistance;
            destinations = setdiff(destinations,current);        
            route = [ route nextSegment ];
        end
    end
  3. 编写函数来修改持久数据

    编写一个函数来添加新城市。添加城市会修改存储在数据缓存中的图形。因为此操作需要写入缓存,所以使用步骤 1 中描述的 mps.sync.mutex 函数进行锁定。添加城市后,通过确认每对城市之间的距离已知来检查图形是否仍然完整。

    使用 addDestination 函数添加城市。添加一个城市会添加一个新的图形节点 name 以及将该节点连接到图中所有现有节点的新边。新添加的边的权重由向量 distances 给出。destinations 是一个字符向量元胞数组,其中包含图中其他城市的名称。

    function count = addDestination(name, destinations, distances)  
        count = 0;
        c = mps.cache.connect('TravelingSalesman','Connection','ScratchPad');   
        if loadDistanceData('ScratchPad','TravelingSalesman') == false
            error('Failed to load distance data. Cannot continue.');
        end
        
        lk = mps.sync.mutex('DistanceData','Connection','ScratchPad');  
        if acquire(lk,10)
            g = c.Distances;      
            newDestinations = setdiff(g.Nodes.Name, destinations);       
            if ~isempty(newDestinations)
                error('MPS:Example:TSP:MissingDestinations', ...
                      'Add distances for missing destinations: %s', ...
                    strjoin(newDestinations,', '));
            end
            
            src = repmat({name},1,numel(destinations));
            g = addedge(g, src, destinations, distances);
            c.Distances = g;
            release(lk);
            count = numnodes(g);
        end   
    end
    

  4. 编写 MATLAB 应用程序来调用路由查找函数

    编写一个 MATLAB App,将步骤 2 和 3 中描述的函数包装在各自的代理函数中。该应用程序允许您指定主机和端口。为了测试,当主机为空且端口值为 0 时,调用本地版本的路由查找函数。对于部署工作流,在指定主机和端口上运行的服务器上调用已部署的函数。使用 webwrite (MATLAB) 函数向服务器发送 HTTP POST 请求。

    有关如何编写应用程序的更多信息,请参阅使用 App 设计工具创建并运行简单 App (MATLAB)

    分别为 findRouteProxyaddDestinationProxylistDestinationProxy 函数编写代理函数 findRouteaddDestinationlistDestination

    function destinations = listDestinationsProxy(app)
        if isempty(app.HostEditField.Value) && ...
                app.PortEditField.Value <= 0
            destinations = listDestinations();
            return;
        end
            
        listDestinations_OPTIONS = weboptions('MediaType','application/json','Timeout',60,'ContentType','raw');
        listDestinations_HOST = app.HostEditField.Value;
        listDestinations_PORT = app.PortEditField.Value;
        noInputJSON = '{ "rhs": [], "nargout": 1 }';
        destinations_JSON = webwrite(sprintf('http://%s:%d/TravelingSalesman/listDestinations', ...
            listDestinations_HOST,listDestinations_PORT), noInputJSON, listDestinations_OPTIONS);
        if iscolumn(destinations_JSON), destinations_JSON = destinations_JSON'; end
        destinations_RESPONSE = mps.json.decoderesponse(destinations_JSON);
        if isstruct(destinations_RESPONSE)
            error(destinations_RESPONSE.id,destinations_RESPONSE.message);
        else
            if nargout > 0, destinations = destinations_RESPONSE{1}; end
        end        
    end
    function [route,distance] = findRouteProxy(app,start,destinations)
        if isempty(app.HostEditField.Value) && ...
                app.PortEditField.Value <= 0
            [route,distance] = findRoute(start,destinations);
            return;
        end  
        findRoute_OPTIONS = weboptions('MediaType','application/json','Timeout',60,'ContentType','raw');
        findRoute_HOST = app.HostEditField.Value;
        findRoute_PORT = app.PortEditField.Value;
        start_destinations_DATA = {};
        if nargin > 0, start_destinations_DATA = [ start_destinations_DATA { start } ]; end
        if nargin > 1, start_destinations_DATA = [ start_destinations_DATA { destinations } ]; end
        route_distance_JSON = webwrite(sprintf('http://%s:%d/TravelingSalesman/findRoute', ...
                findRoute_HOST,findRoute_PORT), ...
                mps.json.encoderequest(start_destinations_DATA,'nargout',nargout), findRoute_OPTIONS);
        if iscolumn(route_distance_JSON), route_distance_JSON = route_distance_JSON'; end
        route_distance_RESPONSE = mps.json.decoderesponse(route_distance_JSON);
        if isstruct(route_distance_RESPONSE)
            error(route_distance_RESPONSE.id,route_distance_RESPONSE.message);
        else
            if nargout > 0, route = route_distance_RESPONSE{1}; end
            if nargout > 1, distance = route_distance_RESPONSE{2}; end
        end
    end
    function count = addDestinationProxy(app, name, destinations,distances)
        if isempty(app.HostEditField.Value) && ...
                app.PortEditField.Value <= 0
            count = addDestination(name, destinations,distances);
            return;
        end
                        
        addDestination_OPTIONS = weboptions('MediaType','application/json','Timeout',60,'ContentType','raw');
        addDestination_HOST = app.HostEditField.Value;
        addDestination_PORT = app.PortEditField.Value;
        name_destinations_distances_DATA = {};
        if nargin > 0, name_destinations_distances_DATA = [ name_destinations_distances_DATA { name } ]; end
        if nargin > 1, name_destinations_distances_DATA = [ name_destinations_distances_DATA { destinations } ]; end
        if nargin > 2, name_destinations_distances_DATA = [ name_destinations_distances_DATA { distances } ]; end
        count_JSON = webwrite(sprintf('http://%s:%d/TravelingSalesman/addDestination', ...
            addDestination_HOST,addDestination_PORT), ...
            mps.json.encoderequest(name_destinations_distances_DATA,'nargout',nargout), addDestination_OPTIONS);
        if iscolumn(count_JSON), count_JSON = count_JSON'; end
        count_RESPONSE = mps.json.decoderesponse(count_JSON);
        if isstruct(count_RESPONSE)
            error(count_RESPONSE.id,count_RESPONSE.message);
        else
            if nargout > 0, count = count_RESPONSE{1}; end
        end
    end

步骤 2:在测试工作流中运行示例

在 MATLAB 桌面环境中测试示例代码。为此,将位于 $MPS_INSTALL/client/matlab/examples/persistence/TravelingSalesman 的所有文件复制到系统上的可写文件夹中,例如 /tmp/persistence_example。启动 MATLAB 桌面并使用 cd (MATLAB) 命令将当前工作目录设置为 /tmp/persistence_example

为了测试目的,使用 mps.cache.control 函数从 MATLAB 桌面控制持久性服务。此函数返回一个 mps.cache.Controller 对象,该对象管理本地持久性服务的生命周期。

  1. 为使用 Redis™ 持久性提供程序的本地持久性服务创建 mps.cache.Controller 对象。

    >> ctrl = mps.cache.control('ScratchPad', 'Redis', 'Port', 8675);

    当处于活动状态时,此控制器将启用名为 ScratchPad 的连接。连接名称将缓存链接到持久性服务中的存储位置。mps.cache.connect 函数需要连接名称来创建数据缓存。MATLAB Production Server 管理员在缓存配置文件 mps_cache_config 中设置连接名称。有关详细信息,请参阅配置服务器以使用 Redis。通过在 MATLAB 桌面会话中使用相同的连接名称,您可以使您的代码从开发阶段转移到测试阶段再到生产阶段而无需进行任何更改。

  2. 使用 start 启动持久性服务。

    >> start(ctrl);
  3. 启动使用持久性服务的 TravelingSalesman 路由查找应用程序。

    >> TravelingSalesman

    应用程序以主机端口的默认值启动。

    点击加载城市以加载城市列表。使用开始菜单设置起始位置并使用 >><< 按钮选择和清除要访问的城市。点击计算路径可显示一条访问所有城市的路由。

    Traveling Salesman app displaying the shortest route

  4. 关闭应用程序时,使用 stop 停止持久性服务。停止持久性服务将删除该服务存储的数据。

    >> stop(ctrl);

步骤 3:在部署工作流中运行示例

要在部署工作流中运行示例,请将位于 $MPS_INSTALL/client/matlab/examples/persistence/TravelingSalesman 的所有文件复制到系统中的可写文件夹,例如 /tmp/persistence_example 。启动 MATLAB 桌面并使用 MATLAB cd (MATLAB) 命令将当前工作目录设置为 /tmp/persistence_example

部署工作流管理 MATLAB 桌面环境之外的持久性服务的生命周期,并调用部署到服务器的存档中打包的路由查找函数。

  1. 创建一个 MATLAB Production Server 实例

    使用 mps-new 从系统命令行创建服务器。有关详细信息,请参阅使用命令行创建服务器实例。如果您尚未设置服务器环境,请参阅 mps-setup 了解更多信息。

    在文件夹 server_1 中创建一个新服务器 tmp

    mps-new /tmp/server_1

    或者,使用 MATLAB Production Server 仪表板创建服务器。有关详细信息,请参阅设置并登录 MATLAB Production Server 仪表板

  2. 创建持久性服务连接

    可部署存档需要一个名为 ScratchPad 的持久性服务连接。使用仪表板创建 ScratchPad 连接,或将示例目录中的 mps_cache_config 文件复制到服务器实例的 config 目录中。如果 config 目录中已经有一个 mps_cache_config 文件,请对其进行编辑,添加 ScratchPad 连接,如 mps_cache_config 示例中指定的那样。

  3. 使用 Production Server Compiler App 创建可部署存档并将其部署到服务器

    1. 打开 Production Server 存档编译器

      • MATLAB 工具条:在 App 选项卡的应用程序部署 下,点击 Production Server 存档编译器

      • MATLAB 命令提示符:输入 productionServerArchiveCompiler

    2. Exported Functions 字段中,添加 findRoute.mlistDestinations.maddDestination.m

    3. 将存档重命名为 TravelingSalesman

    4. Custom Requirements 下,添加 Distances.xlsx

    5. 点击打包

    6. 生成的可部署存档 TravelingSalesman.ctf 位于工程的输出文件夹中。将 TravelingSalesman.ctf 文件复制到服务器的 auto_deploy 文件夹(本例中为 /tmp/server_1/auto_deploy 进行托管。

  4. 启动服务器实例

    使用 mps-start 从系统命令行启动服务器。

    mps-start -C /tmp/server_1
    或者,使用仪表板启动服务器。

  5. 启动持久服务

    使用 mps-cache 从系统命令行启动持久性服务。

    mps-cache start -C /tmp/server_1 --connection ScratchPad
    或者,使用仪表板启动并附加持久性服务。

  6. 测试 App

    启动使用持久性服务的 TravelingSalesman 路由查找应用程序。

    >> TravelingSalesman

    该应用程序以主机端口的空值启动。请参阅位于 main_config 服务器配置文件 server_name/config 以获取 MATLAB Production Server 实例的主机和端口值。对于此示例,在 /tmp/server_1/config 找到配置文件。在应用程序中输入主机和端口值。

    点击加载城市以加载城市列表。使用开始菜单设置起始位置并使用 >><< 按钮选择和清除要访问的城市。点击计算路径可显示一条访问所有城市的路由。

    Traveling Salesman app displaying the shortest route

测试环境工作流和部署环境工作流的结果相同。

另请参阅

(MATLAB Compiler SDK) | (MATLAB Compiler SDK) | (MATLAB Compiler SDK) | (MATLAB Compiler SDK) | (MATLAB Compiler SDK) | (MATLAB Compiler SDK) | (MATLAB Compiler SDK)

主题