分析基于轮询的 Web 应用的限制

已完成

基于轮询的 Web 应用程序。

应用程序的当前体系结构通过基于计时器从服务器提取所有股票价格信息来报告股票信息。 这种设计通常称为基于轮询的设计。

服务器

股票价格信息存储在 Azure Cosmos DB 数据库中。 当由 HTTP 请求触发时,函数 getStocks 返回数据库中的所有行。

import { app, input } from "@azure/functions";

const cosmosInput = input.cosmosDB({
    databaseName: 'stocksdb',
    containerName: 'stocks',
    connection: 'COSMOSDB_CONNECTION_STRING',
    sqlQuery: 'SELECT * from c',
});

app.http('getStocks', {
    methods: ['GET'],
    authLevel: 'anonymous',
    extraInputs: [cosmosInput],
    handler: (request, context) => {
        const stocks = context.extraInputs.get(cosmosInput);
        
        return {
            jsonBody: stocks,
        };
    },
});
  • 获取数据:代码的第一部分 cosmosInput 使用查询 SELECT * from c 获取 Cosmos DB 中 stocksdb 数据库中 stocks 表中的所有项
  • 返回数据:代码的第二部分 app.http 将该数据作为 context.extraInputs 中的输入接收到函数中,然后将其作为响应正文返回给客户端

客户端

示例客户端使用 Vue.js 编制 UI,并使用 Fetch 客户端来处理对 API 的请求。

HTML 页面使用计时器每五秒向服务器发送一次请求,以请求股票。 响应返回一个股票数组,然后显示给用户。

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/bulma/0.7.4/css/bulma.min.css" integrity="sha256-8B1OaG0zT7uYA572S2xOxWACq9NXYPQ+U5kHPV1bJN4=" crossorigin="anonymous" />
    <link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.8.1/css/all.css" integrity="sha384-50oBUHEmvpQ+1lW4y57PTFmhCaXp0ML5d60M1M7uH2+nqUivzIebhndOJK28anvf" crossorigin="anonymous">
    <link rel="stylesheet" href="style.css">
    <title>Stocks | Enable automatic updates in a web application using Azure Functions and SignalR</title>
</head>
<body>
    
    <!-- BEGIN: Replace markup in this section -->
    <div id="app" class="container">
        <h1 class="title">Stocks</h1>
        <div id="stocks">
            <div v-for="stock in stocks" class="stock">
                <div class="lead">{{ stock.symbol }}: ${{ stock.price }}</div>
                <div class="change">Change:
                    <span :class="{ 'is-up': stock.changeDirection === '+', 'is-down': stock.changeDirection === '-' }">
                        {{ stock.changeDirection }}{{ stock.change }}
                    </span></div>
            </div>
        </div>
    </div>
    <!-- END  -->

    <script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.6.10/vue.min.js" integrity="sha256-chlNFSVx3TdcQ2Xlw7SvnbLAavAQLO0Y/LBiWX04viY=" crossorigin="anonymous"></script>
    <script src="bundle.js" type="text/javascript"></script>
</body>
</html>
import './style.css';

function getApiUrl() {

    const backend = process.env.BACKEND_URL;
    
    const url = (backend) ? `${backend}` : ``;
    return url;
}

const app = new Vue({
    el: '#app',
    interval: null,
    data() { 
        return {
            stocks: []
        }
    },
    methods: {
        async update() {
            try {
                
                const url = `${getApiUrl()}/api/getStocks`;
                console.log('Fetching stocks from ', url);

                const response = await fetch(url);
                if (!response.ok) {
                    throw new Error(`HTTP error! status: ${response.status}`);
                }

                app.stocks = await response.json();
            } catch (ex) {
                console.error(ex);
            }
        },
        startPoll() {
            this.interval = setInterval(this.update, 5000);
        }
    },
    created() {
        this.update();
        this.startPoll();
    }
});

startPoll 方法开始轮询后,每五秒调用一次 update 方法。 在 update 方法内,GET 请求发送到 /api/getStocks API 终结点,且结果设置为 app.stocks 以更新 UI。

服务器和客户端代码相对简单:获取所有数据,显示所有数据。 正如我们在分析中发现的那样,这种简单性带来了一些限制。

原型解决方案分析

作为 Tailwind Traders 工程师,你已经发现了这种基于计时器的轮询方法的一些缺点。

  • 不必要的 API 请求:在基于计时器的轮询原型中,无论底层数据是否发生了更改,客户端应用程序都会联系服务器。

  • 不必要的页面刷新:从服务器返回数据后,即使数据没有发生更改,整个股票列表也会在网页上更新。 此轮询机制是一种低效的解决方案。

  • 轮询间隔:为场景选择最佳轮询间隔也有一定难度。 轮询迫使你在每次调用后端所需的成本与你希望应用响应新数据的速度之间做出选择。 新数据可用的时间和应用检测到数据的时间之间通常存在延迟。 下图显示了该问题。

    插图显示时间线和轮询触发器每隔五分钟检查一次新数据。七分钟后可以获得新数据。应用直到下一次轮询(10 分钟后)才会知道是否有新数据。

    在最坏的情况下,检测新数据的潜在延迟时间等于轮询间隔。 那么为什么不使用较小的间隔?

  • 数据量:随着应用程序缩放,在客户端和服务器之间交换的数据量将成为问题。 每个 HTTP 请求标头都包含数百字节的数据以及会话的 cookie。 所有这些开销,尤其是在负载较重的情况下,都会造成资源浪费和服务器不必要的负担。

现在,你对原型已经比较熟悉了,可以让应用程序在计算机上运行了。

支持 CORS

在 Functions 应用的 local.settings.json 文件中,Host 部分包含以下设置。

{
  "IsEncrypted": false,
  "Values": {
    "AzureWebJobsStorage": "<STORAGE_CONNECTION_STRING>",
    "FUNCTIONS_WORKER_RUNTIME": "node",
    "AzureWebJobsFeatureFlags": "EnableWorkerIndexing",
    "COSMOSDB_CONNECTION_STRING": "<COSMOSDB_CONNECTION_STRING>"
  },
  "Host" : {
    "LocalHttpPort": 7071,
    "CORS": "http://localhost:3000",
    "CORSCredentials": true
  }
}

此配置允许在 localhost:3000 运行的 Web 应用程序向在 localhost:7071 运行的函数应用发出请求。 属性 CORSCredentials 告知函数应用接受来自请求的凭据 cookie。

跨域资源共享 (CORS) 是一项 HTTP 功能,使在一个域中运行的 Web 应用程序能够访问另一个域中的资源。 Web 浏览器实施一种称为同源策略的安全限制,以防止网页调用另一个域中的 API;CORS 提供了一种安全的方法来允许一个域(源域)调用另一个域中的 API。

在本地运行时,CORS 是在示例的 local.settings.json 文件中进行配置的,该文件永远不会发布。 部署客户端应用(单元 7)时,还必须更新函数应用中的 CORS 设置,以允许从客户端应用进行访问。