利用 Bokeh 和 BigQuery 创建自定义互动信息中心

在本教程中,您将了解如何使用 Bokeh 库在 Google Cloud Platform (GCP) 上构建一个自定义交互式信息中心应用,以直观展示可公开访问的 BigQuery 数据集中的数据。另外,您还将了解如何在同时确保安全性和可扩缩性的情况下部署此应用。

在构建互动数据可视化信息中心时,强大的软件即服务 (SaaS) 解决方案(如 Google 数据洞察)非常有用。但是,这些类型的解决方案可能无法提供足够的自定义选项。在这种情况下,您可以使用 D3.jsChart.jsBokeh 等开源库来创建自定义信息中心。虽然这些库为构建具有定制功能和可视化特性的信息中心提供了很大的灵活性,但其仍然存在一个主要挑战,即如何将这些信息中心部署到互联网以便利益相关者进行访问。

目标

费用

本教程使用 Google Cloud Platform 的以下收费组件:

  • BigQuery
  • Compute Engine
  • Cloud Storage
  • Cloud CDN
  • 负载平衡器
  • 静态 IP 地址

您可以使用价格计算器根据您的预计使用情况来估算费用。 GCP 新用户可能有资格申请免费试用

准备工作

  1. 选择或创建 Google Cloud Platform 项目。

    转到“管理资源”页面

  2. 确保您的 Google Cloud Platform 项目已启用结算功能。

    了解如何启用结算功能

  3. 启用GKE 和 BigQuery API。

    启用 API

  4. 启动 Cloud Shell 实例。您将在本教程中使用 Cloud Shell。

    打开 Cloud Shell

  5. 通过在 Cloud Shell 中运行以下命令来克隆 Git 代码库:

    git clone https://github.com/GoogleCloudPlatform/bigquery-bokeh-dashboard
  6. 在 Cloud Shell 中,将默认 Compute Engine 地区设置为要创建 GKE 集群的地区。本教程使用 us-central1-a 地区。

    gcloud config set compute/zone us-central1-a
  7. 如需允许演示应用访问 BigQuery,请运行以下命令创建一个服务帐号密钥:

    export PROJECT_ID=$(gcloud info --format='value(config.project)')
    export SERVICE_ACCOUNT_NAME="dashboard-demo"
    cd bigquery-bokeh-dashboard
    gcloud iam service-accounts create $SERVICE_ACCOUNT_NAME \
        --display-name="Bokeh/BigQuery Dashboard Demo"
    gcloud projects add-iam-policy-binding $PROJECT_ID \
        --member="serviceAccount:${SERVICE_ACCOUNT_NAME}@${PROJECT_ID}.iam.gserviceaccount.com" \
        --role roles/bigquery.user
    gcloud iam service-accounts keys create --iam-account \
        "${SERVICE_ACCOUNT_NAME}@${PROJECT_ID}.iam.gserviceaccount.com" \
        service-account-key.json
          

    如需了解服务帐号,请参阅服务帐号

理解演示应用

演示应用展示的信息中心,用于处理和显示美国所有 50 个州的公开可用数据。此信息中心可实现简单的互动,用户可以从下拉列表微件中选择 50 个州中的任意一个,展开并显示有关此州的信息。此信息中心包含四个不同的模块,每个模块显示不同类型的信息:

运行演示的屏幕截图如下:

显示图表的应用的信息中心视图。

整体架构

概括来讲,演示架构由六大元素组成。下图展示了这些组件如何交互:

演示应用的架构。

这些元素包括:

  • 信息中心用户的桌面设备和移动设备上的客户端网络浏览器。
  • Cloud IAP 管理的身份验证代理,负责控制对应用的访问。
  • HTTPS 负载平衡器,负责将传入的 Web 流量有效地分发到合适的后端,以及处理进出系统的数据的 SSL 解密/加密。
  • 负责提供信息中心的动态内容(HTML 和图示数据)的应用后端。
  • 提供客户端网络浏览器呈现所需的静态 JavaScript 和 CSS 文件的静态资源后端。

应用后端

应用后端接收来自负载平衡器的传入请求,然后从 BigQuery 获取和转换相应的数据。后端以 HTTP 上的 HTML 的形式返回请求的动态内容,并通过 WebSocket 返回图示数据。

下图展示了此架构:

应用后端的架构。

应用后端包含两个主要子组件:

  • Bokeh 服务。此服务是应用的核心,包含运行 Bokeh 服务器的 pod,这些服务器可查询来自 BigQuery 的数据并生成客户端网络浏览器使用的动态内容。
  • Memcached 服务。此服务包含运行 Memcached 服务器的 pod,这些服务器负责缓存频繁请求的数据。

这些子组件存在于 Docker 容器中,并作为 Kubernetes pod 部署到 Kubernetes Engine 以实现横向可伸缩性。

Bokeh 服务

在此演示中,Bokeh 服务包含两个 Bokeh pod。每个 pod 都运行一个单独的 Bokeh 服务器实例,其为 Tornado 的非阻塞 Web 服务器的封装容器。此服务器可以通过 HTTP 同步传送 HTML 内容,并通过 Websocket 异步传送信息中心图示数据。

在本教程中,每台 Bokeh 服务器均经过配置,分为四个不同的工作子进程,由容器 DockerfileCMD 语句中的 --num-procs 参数来反映:

CMD bokeh serve --disable-index-redirect --num-procs=4 --port=5006 --address=0.0.0.0 --allow-websocket-origin=$DASHBOARD_DEMO_DOMAIN dashboard.py

Bokeh 服务器对子进程之间的传入流量自动进行负载平衡。这种方法可以提高每个 pod 的性能和弹性。本教程中使用的子进程数是任意的。在实际使用场景中,您可以根据实际生产流量以及集群中可用的内存和 CPU 资源来调整此数量。

使用 read_gbq 方法可以直接从 Bokeh 应用访问 BigQuery。如需了解详情,请参阅 Pandas 库的 gbq 扩展程序中提供的 read_gbq 方法的相关文档。这种方法有一个非常简单的接口。它接受将查询作为参数,并通过网络将其提交给 BigQuery。其他必需参数包括项目 ID 和服务帐号密钥,这两个参数都可以通过从 GKE 密钥加载的环境变量来传递。然后,read_gbq 方法以透明方式处理 BigQuery 返回的结果的分页,并将它们合并到单个 Pandas DataFrame 对象中,最后由 Bokeh 直接处理该对象。

以下是使用 read_gbq 提交的查询的示例,此查询用于收集加利福尼亚州(州代码为“CA”)人口最多的 100 个邮政编码区域:

query = """
    SELECT
      A.zipcode,
      Population,
      City,
      State_code
    FROM
      `bigquery-public-data.census_bureau_usa.population_by_zip_2010` AS A
    JOIN
      `bigquery-public-data.utility_us.zipcode_area` AS B
    ON
     A.zipcode = B.zipcode
    WHERE
     gender = ''
    AND
     state_code = '%(state)s'
    ORDER BY
     population DESC
    LIMIT
     100
"""

import os
import pandas
dataframe = pandas.io.gbq.read_gbq(
   query % 'CA',
   project_id=os.environ['GOOGLE_PROJECT_ID'],
   private_key=os.environ['GOOGLE_APPLICATION_CREDENTIALS'],
   dialect='standard'
)

请注意,由于生成的数据需要通过网络传输,因此强烈建议尽可能多地将预处理数据分流至 BigQuery。例如,在查询层级执行必要的降采样(通过使用 LIMIT 子句,定位 SELECT 子句中的字段,预分段表或使用 GROUP BY 子句预分组结果)以减少整体网络流量并加快响应时间。

回顾一下,演示信息中心展示了四个不同的图示模块。当用户选择美国的某个州时,应用向 BigQuery 发送四个单独的查询(每个模块一个),总共处理 7GB 的数据。运行每个 BigQuery 查询平均需要 1 到 3 秒。在此处理过程中,应用保持空闲状态,等待结果从 BigQuery 返回。为了优化信息中心的总体响应时间,这四个查询在不同的线程上并行运行,如以下代码所示:

def fetch_data(state):
    """
    Fetch data from BigQuery for the given US state by running
    the queries for all dashboard modules in parallel.
    """
    t0 = time.time()
    # Collect fetch methods for all dashboard modules
    fetch_methods = {module.id: getattr(module, 'fetch_data') for module in modules}
    # Create a thread pool: one separate thread for each dashboard module
    with concurrent.futures.ThreadPoolExecutor(max_workers=len(fetch_methods)) as executor:
        # Prepare the thread tasks
        tasks = {}
        for key, fetch_method in fetch_methods.items():
            task = executor.submit(fetch_method, state)
            tasks[task] = key
        # Run the tasks and collect results as they arrive
        results = {}
        for task in concurrent.futures.as_completed(tasks):
            key = tasks[task]
            results[key] = task.result()
    # Return results once all tasks have been completed
    t1 = time.time()
    timer.text = '(Execution time: %s seconds)' % round(t1 - t0, 4)
    return results

当从 BigQuery 收到所有结果后,Bokeh 的图表库将绘制数据。例如,以下代码绘制空气污染水平随时间的变化:

def make_plot(self, dataframe):
    self.source = ColumnDataSource(data=dataframe)
    palette = all_palettes['Set2'][6]
    hover_tool = HoverTool(tooltips=[
        ("Value", "$y"),
        ("Year", "@year"),
    ])
    self.plot = figure(
        plot_width=600, plot_height=300, tools=[hover_tool],
        toolbar_location=None)
    columns = {
        'pm10': 'PM10 Mass (µg/m³)',
        'pm25_frm': 'PM2.5 FRM (µg/m³)',
        'pm25_nonfrm': 'PM2.5 non FRM (µg/m³)',
        'lead': 'Lead (¹/₁₀₀ µg/m³)',
    }
    for i, (code, label) in enumerate(columns.items()):
        self.plot.line(
            x='year', y=code, source=self.source, line_width=3,
            line_alpha=0.6, line_color=palette[i], legend=label)

    self.title = Paragraph(text=TITLE)
    return column(self.title, self.plot)

部署 Bokeh 服务

为提高应用的横向可伸缩性,应用后端的所有组件均使用 GKE 通过 Docker 容器进行部署。下面是将这些组件部署到 GKE 的步骤。

  1. 首先,在 Cloud Shell 中运行以下命令,创建一个有 2 个(任意选定的数字)节点的 GKE 集群:

    gcloud container clusters create dashboard-demo-cluster \
      --tags dashboard-demo-node \
      --num-nodes=2
    
  2. 创建 GKE 密钥以安全地存储您之前创建的项目 ID 和服务帐号密钥。

    export PROJECT_ID=$(gcloud info --format='value(config.project)')
    
    kubectl create secret generic project-id \
      --from-literal project-id=$PROJECT_ID
    
    kubectl create secret generic service-account-key \
      --from-file service-account-key=service-account-key.json
    
  3. 创建一个静态 IP 地址:

    gcloud compute addresses create dashboard-demo-static-ip --global
    
  4. 使用 xip.io 定义一个网域,xip.io 是一个免费的 DNS 服务,可自动将包含 IP 地址的网域解析回这一 IP 地址:

    export STATIC_IP=$(gcloud compute addresses describe \
      dashboard-demo-static-ip --global --format="value(address)")
    
    export DOMAIN="${STATIC_IP}.xip.io"
    
    echo "My domain: ${DOMAIN}"
    
  5. 使用该网域创建一个 GKE 密钥:

    kubectl create secret generic domain --from-literal domain=$DOMAIN
    
  6. 现在可以使用以下命令部署 Bokeh 服务:

    kubectl create -f kubernetes/bokeh.yaml
    

最后一步为部署任意数量的 pod(此处为 2 个),每个 pod 运行一个单独的演示应用实例。它还将先前通过 env.valueFrom.secretKeyRef 条目创建的三个 GKE 密钥公开为环境变量,以便应用可以检索此信息以访问 BigQuery。

如需了解详情,请参阅 bokeh.yaml

Memcached 服务

如果您预计信息中心上显示的数据在给定的时间段内不会发生变化,那么请务必考虑性能和费用的优化。为此,默认情况下,BigQuery 会持续 24 小时在内部缓存来自重复查询的结果。这大幅缩短了响应时间并避免产生不必要的费用。如需了解详情,请参阅 Memcached 服务相关文档中关于此缓存机制的信息。

通过在信息中心应用的内存中本地缓存常用数据,可以进一步改进此缓存机制。这种方法使 BigQuery 跳出了循环,避免了使用不必要的网络流量收集缓存数据。本教程使用 Memcached 来处理这种类型的应用层级缓存。Memcached 速度非常快,在本教程的示例中,可将响应时间从几秒减少到几毫秒。

Memcached 的一个特点是它没有内置的负载平衡机制。反之,多个 Memcached pod 之间的负载平衡必须由 Memcached 客户端完成,该客户端包含在每个 Bokeh pod 中,如下图所示:

Memchached pod 的架构。

对于要让 Memcached 客户端发现的 Memcached pod,必须将 Memcached 服务声明为无头,这可以通过使用 GKE 配置文件中的 clusterIP: None 条目来实现(请参阅 memcached.yam)。然后,Memcached 客户端可以通过查询 kube-dns 中的 memcached.default.svc.cluster.local 域名来检索各个 pod 的 IP 地址。

部署 Memcached 服务

与 Bokeh 服务类似,Memcached 服务使用 Kubernetes 通过 Docker 容器部署到 GKE。

通过运行以下命令来部署 Memcached pod 和无头服务:

kubectl create -f kubernetes/memcached.yaml

添加负载平衡器

此时,信息中心应用在专用服务中运行。您需要引入 HTTPS 负载平衡器将应用公开到互联网,以便用户能够访问信息中心。您将通过以下步骤实现此目的:

  1. 创建 SSL 证书。
  2. 创建负载平衡器。
  3. 延长连接超时。

创建 SSL 证书

在实际使用场景中,信息中心显示的数据可以是敏感数据或私有数据。在通过互联网将此数据发送给用户之前,您应使用 SSL 对其加密。对此,您首先要创建 SSL 证书。

在生产设置中,您必须通过官方证书授权机构颁发证书。在本教程中,您可以通过运行以下命令来颁发自签名证书

export STATIC_IP=$(gcloud compute addresses describe \
  dashboard-demo-static-ip --global --format="value(address)")

export DOMAIN="${STATIC_IP}.xip.io"

mkdir /tmp/dashboard-demo-ssl

cd /tmp/dashboard-demo-ssl

openssl genrsa -out ssl.key 2048

openssl req -new -key ssl.key -out ssl.csr -subj "/CN=${DOMAIN}"

openssl x509 -req -days 365 -in ssl.csr -signkey ssl.key -out ssl.crt

cd -

创建 HTTPS 负载平衡器

HTTPS 负载平衡器包含多个资源,具体如下:

  • 全局转发规则。
  • 运行状况检查。
  • 后端服务。
  • 网址映射。
  • 目标代理。

要同时创建 GitHub 源代码库中包含的所有这些资源,请运行以下命令:

./create-lb.sh

访问此命令返回的网址,验证信息中心处于活动状态:

echo https://${STATIC_IP}.xip.io/dashboard

如果您的浏览器返回带有 502 代码的错误,则表示负载平衡器仍在部署中。此部署可能需要几分钟才能完成。等待几秒钟,然后刷新页面。重复此步骤,直到页面正常运行并显示信息中心。

此时,HTTPS 负载平衡器会终止加密。进出系统的流量使用 HTTPS 和 Secure WebSocket,而系统内的流量使用常规 HTTP 和 WebSocket 保持未加密状态。在大多数情况下,此级别的加密已经足够。如果您需要进一步增强安全性,请考虑在 GCP 项目的虚拟私有云网络中执行 SSL 加密。在 VPC 网络中设置 SSL 加密不在本教程的讨论范围内。

延长连接超时

默认情况下,HTTPS 负载平衡器会关闭所有打开超过 30 秒的连接

如需延长信息中心的 WebSocket 连接保持打开的时间,您需要增长连接超时。以下 gcloud 命令将超时设置为一天(86400 秒):

gcloud compute backend-services update dashboard-demo-service \
    --global --timeout=86400

静态资源后端

Bokeh pod 可以直接提供呈现信息中心所需的所有静态 JavaScript 和 CSS 文件。但是,让这些 pod 处理此任务存在以下弊端:

  • 它会为 Bokeh pod 带去不必要的负担,可能会降低它们提供其他类型资源的能力,例如动态图示数据。
  • 它会强制客户端从同一源服务器下载资源,而不管客户端所在的地理位置。

若要克服这些弊端,请创建一个单独的专用后端来高效地提供静态资源。

此静态资源后端使用以下两个主要组件:

  • Cloud Storage 中托管的存储分区,负责保存源 JavaScript 和 CSS 资源。
  • Cloud CDN,负责将这些资源全球分发到靠近信息中心潜在用户的位置,从而减少延迟时间并加快页面加载速度。

下图展示了此架构:

静态资源后端的架构。

创建静态资源后端

  1. 通过运行以下命令从 Bokeh 容器中提取静态资源,并将它们本地存储在临时目录中:

    gcloud auth configure-docker --quiet
    
    export DOCKER_IMAGE="gcr.io/cloud-solutions-images/dashboard-demo"
    
    export STATIC_PATH=$(docker run $DOCKER_IMAGE bokeh info --static)
    
    export TMP_PATH="/tmp/dashboard-demo-static"
    
    mkdir $TMP_PATH
    
    docker run $DOCKER_IMAGE tar Ccf $(dirname $STATIC_PATH) - \
        static | tar Cxf $TMP_PATH -
    
  2. 使用 Cloud SDK 中包含的 gsutil 命令创建存储分区,并将所有资源上传到该存储分区:

    export BUCKET="${PROJECT_ID}-dashboard-demo"
    
    # Create the bucket
    gsutil mb gs://$BUCKET
    
    # Upload the assets to the bucket
    gsutil -m cp -z js,css -a public-read -r $TMP_PATH/static gs://$BUCKET
    

    -z 选项将 gzip 编码应用于具有 jscss 文件扩展名的文件,这样可以节省 Cloud Storage 中的网络带宽和空间,从而降低存储费用并加快页面加载速度。-a 选项将权限设置为 public-read 以便所有客户端可公开访问上传的资源。

  3. 使用 --enable-cdn 选项创建后端存储分区,包括 Cloud CDN 支持:

    gcloud compute backend-buckets create dashboard-demo-backend-bucket \
        --gcs-bucket-name=$BUCKET --enable-cdn
    
  4. 将后端存储分区连接到 HTTPS 负载平衡器,以服务请求 /static/* 目录内的资源的所有流量:

    gcloud compute url-maps add-path-matcher dashboard-demo-urlmap \
        --default-service dashboard-demo-service \
        --path-matcher-name bucket-matcher \
        --backend-bucket-path-rules="/static/*=dashboard-demo-backend-bucket"
    
  5. 此配置更改可能需要几分钟才能传播。稍等片刻,然后检查静态资源的响应头是否包含 x-goog-* 参数,验证 Cloud CDN 已正常运行:

    curl -sD - -o /dev/null "https://${DOMAIN}/static/js/bokeh.min.js" | grep x-goog
    

身份验证

虽然之前在负载平衡器层级启用的 SSL 加密可保护传输,但我们建议您添加一个身份验证层以控制对应用的访问并防止滥用。您可以启用 Cloud IAP 来实现此目的。

第一步是设置 OAuth 同意屏幕:

  1. 设置 OAuth 同意屏幕:

    配置同意屏幕

    1. 电子邮件地址下,选择要显示为公开联系人的电子邮件地址。这必须是您的电子邮件地址或您拥有的 Google 群组。
    2. 输入“Dashboard Demo”作为产品名称。
    3. 点击保存
  2. 凭据下,依次点击创建凭据 > OAuth 客户端 ID

  3. 应用类型下,选择 Web 应用,然后输入“Dashboard demo”作为名称,并以以下格式指定已获授权的重定向 URI

    https://[STATIC_IP].xip.io/_gcp_gatekeeper/authenticate
    

    [STATIC_IP] 替换为为本教程创建的实际静态 IP 地址。

  4. 输入完详细信息后,点击创建并记下 OAuth 客户端窗口中显示的客户端 ID客户端密钥

  5. 在 Cloud Shell 中运行以下命令以启用 Cloud IAP。将 [CLIENT_ID] 值和 [CLIENT_SECRET] 值替换为上一步中记录的实际值:

    gcloud beta compute backend-services update dashboard-demo-service --global \
        --iap=enabled,oauth2-client-id=[CLIENT_ID],oauth2-client-secret=[CLIENT_SECRET]
    
  6. 最后,添加一个允许访问该应用的成员。将 [EMAIL] 值替换为您的 Google 帐号电子邮件地址:

    gcloud projects add-iam-policy-binding $PROJECT_ID \
        --role roles/iap.httpsResourceAccessor \
        --member user:[EMAIL]
    

此配置更改可能需要几分钟才能传播。完成后,应用会受到 Cloud IAP 的保护,只有拥有上述电子邮件地址的用户才有权访问。请重复上一步的流程以添加更多用户。

清理

为避免因本教程中使用的资源而导致您的 Google Cloud Platform 帐号产生费用,请执行以下操作:

删除项目

为了避免产生费用,最简单的方法是删除您为本教程创建的项目。

如需删除项目,请执行以下操作:

  1. 在 GCP Console 中,转到“项目”页面。

    转到“项目”页面

  2. 在项目列表中,选择要删除的项目,然后点击删除
  3. 在对话框中输入项目 ID,然后点击关闭以删除项目。

删除单个资源

运行以下命令以删除单个资源,而非删除整个项目:

export PROJECT_ID=$(gcloud info --format='value(config.project)')

# Delete the load balancer resources
gcloud compute forwarding-rules \
    delete dashboard-demo-gfr --global --quiet
gcloud compute target-https-proxies \
    delete dashboard-demo-https-proxy --quiet
gcloud compute ssl-certificates \
    delete dashboard-demo-ssl-cert --quiet
gcloud compute url-maps \
    delete dashboard-demo-urlmap --quiet
gcloud compute backend-services \
    delete dashboard-demo-service --global --quiet
gcloud compute health-checks delete \
    dashboard-demo-basic-check --quiet
gcloud compute firewall-rules delete \
    gke-dashboard-demo-lb7-fw --quiet

# Delete the Kubernetes Engine cluster
gcloud container clusters delete dashboard-demo-cluster --quiet

# Delete the service account
export SERVICE_ACCOUNT_NAME="dashboard-demo"
gcloud iam service-accounts delete \
    "${SERVICE_ACCOUNT_NAME}@${PROJECT_ID}.iam.gserviceaccount.com" --quiet

# Delete the backend bucket
gcloud compute backend-buckets delete dashboard-demo-backend-bucket --quiet

# Delete the Cloud Storage bucket
export BUCKET="${PROJECT_ID}-dashboard-demo"
gsutil -m rm -r gs://$BUCKET

# Delete the static IP address
gcloud compute addresses delete dashboard-demo-static-ip --global --quiet

# Delete the local Docker image
docker rmi -f gcr.io/cloud-solutions-images/dashboard-demo

最后,删除 Oauth 客户端 ID:

  1. 打开凭据页面。

    打开“凭据”

  2. 点击“Dashboard demo”条目。

  3. 点击删除,然后再次点击删除以确认。

后续步骤

  • 试用其他 Google Cloud Platform 功能。查阅我们的教程
此页内容是否有用?请给出您的反馈和评价:

发送以下问题的反馈:

此网页
Solutions