作者 | 王夕宁 阿里云高级技术专家
参与阿里巴巴云原生公众号文末留言互动,即有机会获得赠书福利!
**导读:**本文摘自于由阿里云高级技术专家王夕宁撰写的《Istio 服务网格技术解析与实践》一书,在展望服务网格未来的同时,讲述了如何使用 Istio 进行多集群部署管理,来阐述服务网格对多云环境、多集群即混合部署的支持能力。你只需开心参与阿里巴巴云原生公众号文末互动,我们负责买单!技术人必备书籍《Istio 服务网格技术解析与实践》免费领~
服务网格作为一个改善服务到服务通信的专用基础设施层,是云原生范畴中最热门的话题。随着容器愈加流行,服务拓扑也频繁变动,这就需要更好的网络性能。服务网格能够通过服务发现、路由、负载均衡、心跳检测和支持可观测性,帮助我们管理网络流量。服务网格试图为无规则的复杂的容器问题提供规范化的解决方案。
服务网格也可以用于混沌工程 —— “一门在分布式系统上进行实验的学科,目的是构建能够应对极端条件的可靠系统”。服务网格能够将延迟和错误注入到环境中,而不需要在每个主机上安装一个守护进程。
容器是云原生应用的基石,通过应用容器化,使得应用开发部署更加敏捷、迁移更加灵活,并且这些实现都是基于标准化的。而容器编排则是更近一步,能够更加有效地编排资源、更加高效地调度利用这些资源。而到了云原生时代,在 Kubernetes 基础架构之上,结合 Istio 服务网格,提供了多云、混合云的支持能力,针对微服务提供了有效的治理能力,并以 Kubernetes 和 Istio 为基础,提供了针对特定应用负载的不同支持,例如针对 Kubeflow 服务的流量治理、为 Knative 提供负载的路由管理能力等。
尽管 Service Mesh 在云原生系统方面的应用已经有了快速的增长,但仍然存在巨大的提升空间。无服务器(Serverless)计算正好需要 Service Mesh 的命名和链接模型,这让 Service Mesh 在云原生生态系统中的角色得到了彰显。服务识别和访问策略在云原生环境中仍显初级,而 Service Mesh 毫无疑问将成为这方面不可或缺的基础。就像 TCP/IP 一样,Service Mesh 将在底层基础设施这条道路上更进一步。
混合云可以采用多种形式。通常,混合云指的是跨公有云和私有(内部部署)云运行,而多云意味着跨多个公有云平台运行。
采用混合云或多云架构可以为你的组织带来诸多好处。例如,使用多个云提供商可以帮助你避免供应商锁定,能够让你为实现目标选择最佳的云服务。使用云和本地环境,你可以同时享受云的优势(灵活性、可扩展性、成本降低)和本地的好处(安全性、低延迟、硬件复用)。如果你是首次迁移到云端,采用混合云步骤可以让你按照自己的节奏,以最适合你业务的方式进行。
根据我们在公有云上的实践经验及从客户那里得到的信息,我们认为采用混合服务网络是简化云和本地环境中应用程序管理、安全性和可靠性的关键,无论你的应用程序是在容器中运行,或是在虚拟机中运行。
Istio 的一个关键特性是它为你的工作负载(例如 pod、job、基于 VM 的应用程序)提供服务抽象。当你转向混合拓扑时,这种服务抽象变得更加重要,因为现在你不只需要关注一个环境,而是需要关注若干个环境。
当你在一个 Kubernetes 集群上使用 Istio 时,可以获得包括可见性、细粒度流量策略、统一遥测和安全性在内的微服务的所有管理优势。但是当你在多个环境中使用 Istio 时,实际上是为应用程序提供了一个新的超级能力。因为 Istio 不仅仅是 Kubernetes 的服务抽象,也是一种在整个环境中标准化网络的方法。它是一种集中 API 管理并将 JWT 验证与代码分离的方法。它是跨云提供商的安全、零信任网络的快速通道。
那么所有这些魔法是如何发生的呢?混合 Istio 是指一组 Istio Sidecar 代理,每一个 Envoy 代理位于所有服务的旁边,而这些服务可能运行在不同环境中的每一个虚拟机、每一个容器中,而且这些 Sidecar 代理之前互相知道如何跨边界交互。这些 Envoy Sidecar 代理可能由一个中央 Istio 控制平面管理,或由每个环境中运行的多个控制平面管理。
多集群部署管理
服务网格本质上是将一组单独的微服务组合成单个可控的复合应用程序,Istio 作为一种服务网格,也是旨在单一管理域下监视和管理协作微服务网络。对于特定大小的应用程序,所有微服务是可以在单个编排平台如一个 Kubernetes 集群上运行的。然而,由于规模不断增大或者冗余等原因,大多数应用程序最终将需要分发一些服务在其他地方运行。
社区越来越关注在多个集群上运行工作负载,以实现更好的扩展,故障可以更好地隔离,从而提升应用程序的敏捷性。Istio v1.0 开始支持一些多集群功能,并在之后的版本中添加了新功能。
Istio 服务网格支持许多可能的拓扑结构,用于在单个集群之外分发应用程序的服务,有两种常见的模式或用例:单网格和网格联合。顾名思义,单个网格将多个集群组合成一个单元,由一个 Istio 控制平面管理;它可以实现为一个物理控制平面,也可以实现为一组控制平面,同时所有控制平面都能通过复制配置保持同步。而网格联合则会将多个集群分离作为单独的管理域,有选择地完成集群之间的连接,仅将服务的子集暴露给其他集群;自然它的实现会包含多个控制平面。
具体来说,这些不同的拓扑结构包括以下几个方面:
- 网格中的服务可以使用服务条目(Service Entry)来访问独立的外部服务或访问由另一个松散耦合的服务网格公开的服务,通常称为网格联邦(Mesh Federation)。这种拓扑适合于互相独立并且网络隔离、只能通过公网交互的多集群的场景;
- 支持在虚拟机或物理裸机上运行的服务进行服务网格扩展,通常称为网格联合(Mesh Expansion)。在前面章节中,我们已经讲述了这种 Kubernetes 集群与虚拟机、物理裸机之间混合部署的场景;
- 把来自多个集群的服务组合到单个服务网格中,通常称为多集群网格(Multicluster Mesh)。根据网络拓扑结构的不同,多集群网格通常分为单控制平面 VPN 连接、单控制平面网关连接以及多控制平面拓扑。
单控制平面 VPN 连接拓扑
作为基准,在 Istio 的 1.1 版本之前,Istio 1.0 多集群仅支持使用单网格设计。它允许多个集群连接到网格中,但所有集群都在一个共享网络上。也就是说,所有集群中所有 pod 和服务的 IP 地址都是可直接路由的,不会发生冲突,同时保证在一个集群中分配的IP地址不会在另一个集群中同时重用。
在这种拓扑配置下,在其中一个集群上运行单个 Istio 控制平面。该控制平面的 Pilot 管理本地和远程集群上的服务,并为所有集群配置 Envoy 代理。这种方法在所有参与集群都具有 VPN 连接的环境中效果最佳,因此可以使用相同的 IP 地址从其他任何地方访问网格中的每个 pod。
在此配置中,Istio 控制平面部署在其中一个集群上,而所有其他集群运行更简单的远程 Istio 配置,该配置将它们连接到单个 Istio 控制平面,该平面将所有 Envoy 管理为单个网格。各个集群上的 IP 地址不允许重叠,并且远程集群上的服务的 DNS 解析不是自动的。用户需要在每个参与集群上复制服务,这样每个集群中的 Kubernetes 集群服务和应用程序都能够将其内部 Kubernetes 网络暴露给其他集群。一旦一个或多个远程 Kubernetes 集群连接到 Istio 控制平面,Envoy 就可以与单个控制平面通信并形成跨多个集群的网状网络。
前提约束
事实上,我们已经了解到网格、集群和网络之间的存在各种约束,例如,在某些环境中,网络和集群直接相关。Istio单网格设计下的单控制平面VPN连接拓扑需要满足以下几个条件:
- 运行 Kubernetes 1.9 或更高版本的两个或更多集群;
- 能够在其中一个集群上部署 Istio 控制平面;
- RFC1918 网络、VPN 或满足以下要求的更高级网络技术:
- 单个集群 pod CIDR 范围和服务 CIDR 范围在多集群环境中必须是唯一的,并且应当不重叠;
- 每个集群中的所有 pod CIDR 必须可以相互路由;
- 所有 Kubernetes 控制平面 API 服务器必须可以相互路由。
此外,为了跨集群支持 DNS 名称解析,必须确保在所有需要跨集群服务调用的集群中定义对应的命名空间、服务和服务账户;例如,集群 cluster1 中命名空间 ns1 的服务 service1 需要调用集群 cluster2 中命名空间 ns2 的服务 service2,那么在集群 cluster1 中为了支持服务名的 DNS 解析,需要在集群 cluster1 中创建一个命名空间 ns2 以及该命名空间下的服务 service2。
以下示例中的两个 Kubernetes 集群的网络假定已经满足上述要求,每个集群中的 pod 都能够互相路由,也就是说网络可通并且端口是可访问的(如果采用的是类似于阿里云的公有云服务,请确保这些端口在安全组规则下是可以访问的;否则服务间的调用会受到影响)。
两个 Kubernetes 集群的 pod CIDR 范围和服务 CIDR 范围定义如下表所示:
(CIDR 范围定义)
拓扑架构
(Istio 中单控制平面 VPN 连接拓扑的多集群支持的调用关系)
从图中可以看到整个多集群拓扑中只会在一个 Kubernetes 集群上安装 Istio 控制平面。这个安装 Istio 控制平面的集群通常被称为本地集群,所有其它集群称为远程集群。
这些远程集群只需要安装 Istio 的 Citadel 和 Sidecar Injector 准入控制器,具有较小的 Istio 占用空间,Citadel 用于这些远程集群的安全管理,Sidecar Injector 准入控制器用于控制平面中的自动注入和数据平面中工作负载的 Sidecar 代理功能。
在这个架构中,Pilot 可以访问所有集群中的所有 Kubernetes API 服务器,因此它具有全局网络访问视图。Citadel 和 Sidecar Injector 准入控制器则只会在集群本地范围内运行。每个集群都有唯一的 pod 和服务 CIDR,除此之外,集群之间还有一个共享的扁平网络,以保证能直接路由到任何工作负载,包括到 Istio 的控制平面。例如,远程集群上的 Envoy 代理需要从 Pilot 获得配置,检查并报告给 Mixer 等。
启用双向 TLS 通信
如果在多个集群中启用跨集群的双向 TLS 通信,就需要按照如下方式在各个集群中进行部署配置。首先,从共享的根 CA 为每个集群的 Citadel 生成中间 CA 证书,共享的根 CA 启用跨不同集群的双向 TLS 通信。为了便于说明,我们将 samples/certs 目录下 Istio 安装中提供的示例根 CA 证书用于两个集群。在实际部署中,你可能会为每个集群使用不同的 CA 证书,所有 CA 证书都由公共根 CA 签名。
在每个 Kubernetes 集群中(包括示例中的集群 cluster1 与 cluster2)创建密钥。使用以下的命令为生成的 CA 证书创建 Kubernetes 密钥:
kubectl
create namespace istio-system
kubectl
create secret generic cacerts -n istio-system \
--from-file=samples/certs/ca-cert.pem \
--from-file=samples/certs/ca-key.pem \
--from-file=samples/certs/root-cert.pem \
--from-file=samples/certs/cert-chain.pem
当然,如果你的环境只是开发测试或者不需要启用双向 TLS 通信,上述步骤完全可以跳过。
部署本地控制平面
在所谓的本地集群上安装一个 Istio 控制平面的过程,与在单集群上安装 Istio 并没有太多差别,需要注意的一点是如何配置 Envoy 代理用于管理直接访问某个 IP 范围内的外部服务的参数。如果是使用 Helm 安装 Istio,那么在 Helm 中有一个名为 global.proxy.includeIPRanges 的变量,确保该变量为“*”或者包括本地集群、所有远程集群的 pod CIDR 范围和服务 CIDR。
可以通过查看命名空间 istio-system 下的配置项 istio-sidecar-injector 中的 traffic.sidecar.istio.io/includeOutboundIPRanges 来确认 global.proxy.includeIPRanges 参数设置,如下所示:
kubectl
get configmap istio-sidecar-injector -n istio-system -o yaml| grep
includeOutboundIPRanges
'traffic.sidecar.istio.io/includeOutboundIPRanges' \"*\" ]]\"\n - \"-x\"\n
在部署 Istio 控制平面组件的集群 cluster1 中,按照以下步骤执行。
- 如果启用了双向 TLS 通信,则需要如下配置参数:
helm
template --namespace=istio-system \
--values
install/kubernetes/helm/istio/values.yaml \
--set global.mtls.enabled=true \
--set security.selfSigned=false \
--set global.controlPlaneSecurityEnabled=true
\
install/kubernetes/helm/istio > istio-auth.yaml
kubectl
apply -f istio-auth.yaml
- 如果不需要启用双向 TLS 通信,配置参数则需要做出如下修改:
helm
template --namespace=istio-system \
--values
install/kubernetes/helm/istio/values.yaml \
--set global.mtls.enabled=false \
--set security.selfSigned=true \
--set global.controlPlaneSecurityEnabled=false
\
install/kubernetes/helm/istio >
istio-noauth.yaml
kubectl
apply -f istio-noauth.yaml
修改 Istio 服务 istio-pilot、istio-telemetry、istio-policy 及 zipkin 的类型为内网负载均衡,将这些服务以内网方式暴露给远程集群使用。不同的云厂商实现机制不尽相同,但大都是通过修改 annotation 的方式实现。针对阿里云容器服务来说,设置为内网负载均衡的方式非常简单,只需要添加如下 annotation 到服务的 YAML 定义中即可: service.beta.kubernetes.io/alicloud-loadbalancer-address-type: intranet。
此外,需要按照图中的端口定义为每一个服务进行设置。
(服务设置)
istio-pilot 服务端口如表 1 所示。
(表 1 istio-pilot 服务端口说明)
istio-telemetry 服务端口如表 2 所示
(表 2 istio-telemetry 服务端口说明)
istio-policy 服务端口如表 3 所示。
(表 3 istio-policy 服务端口说明)
zipkin 服务端口如表 4 所示。
(表 4 zipkin 服务端口说明)
安装 istio-remote
在本地集群中安装完控制平面之后,必须将 istio-remote 组件部署到每个远程 Kubernetes 集群。等待 Istio 控制平面完成初始化,然后再执行本节中的步骤。你必须在 Istio 控制平面集群上运行这些操作以捕获 Istio 控制平面服务端点,例如上述提到的 Istio 服务 istio-pilot、istio-telemetry、istio-policy 以及 zipkin。
在远程集群 cluster2 中部署 Istio-remote 组件,按照以下步骤执行:
1.在本地集群上使用以下命令设置环境变量:
export
PILOT_IP=$(kubectl -n istio-system get service istio-pilot -o
jsonpath='{.status.loadBalancer.ingress[0].ip}')
export
POLICY_IP=$(kubectl -n istio-system get service istio-policy -o
jsonpath='{.status.loadBalancer.ingress[0].ip}')
export
TELEMETRY_IP=$(kubectl -n istio-system get service istio-telemetry -o
jsonpath='{.status.loadBalancer.ingress[0].ip}')
export
ZIPKIN_IP=$(kubectl -n istio-system get service zipkin -o
jsonpath='{.status.loadBalancer.ingress[0].ip}')
echo
$PILOT_IP $POLICY_IP $TELEMETRY_IP $ZIPKIN_IP
2.如果在多个集群中启用跨集群的双向 TLS 通信,就需要在集群中进行部署配置。
当然,如果你的环境只是开发测试或者不需要启用双向 TLS 通信的话,该步骤完全可以跳过。在远程 Kubernetes 集群 cluster2 上运行以下命令,在集群中为生成的 CA 证书创建 Kubernetes 密钥:
kubectl
create namespace istio-system
kubectl
create secret generic cacerts -n istio-system \
--from-file=samples/certs/ca-cert.pem \
--from-file=samples/certs/ca-key.pem \
--from-file=samples/certs/root-cert.pem \
--from-file=samples/certs/cert-chain.pem
3.在远程 Kubernetes 集群 cluster2 上,通过执行以下命令,使用 Helm 创建 Istio remote 部署 YAML 文件。
如果启用了双向 TLS 通信,则需要如下配置参数:
helm
template install/kubernetes/helm/istio \
--name istio-remote \
--namespace istio-system \
--values install/kubernetes/helm/istio/values-istio-remote.yaml
\
--set global.mtls.enabled=true \
--set security.selfSigned=false \
--set global.controlPlaneSecurityEnabled=true
\
--set
global.remotePilotCreateSvcEndpoint=true \
--set global.remotePilotAddress=${PILOT_IP} \
--set global.remotePolicyAddress=${POLICY_IP}
\
--set
global.remoteTelemetryAddress=${TELEMETRY_IP}
--set global.remoteZipkinAddress=${ZIPKIN_IP}
> istio-remote-auth.yaml
然后将 Istio remote 组件部署到 cluster2,如下所示:
kubectl
apply -f ./istio-remote-auth.yaml
如果不需要启用双向 TLS 通信,配置参数则需要做出如下修改:
helm
template install/kubernetes/helm/istio \
--name istio-remote \
--namespace istio-system \
--values
install/kubernetes/helm/istio/values-istio-remote.yaml \
--set global.mtls.enabled=false \
--set security.selfSigned=true \
--set
global.controlPlaneSecurityEnabled=false \
--set
global.remotePilotCreateSvcEndpoint=true \
--set global.remotePilotAddress=${PILOT_IP} \
--set global.remotePolicyAddress=${POLICY_IP}
\
--set global.remoteTelemetryAddress=${TELEMETRY_IP}
--set global.remoteZipkinAddress=${ZIPKIN_IP}
> istio-remote-noauth.yaml
然后将 Istio remote 组件部署到 cluster2,如下所示:
kubectl
apply -f ./istio-remote-noauth.yaml
确保上述步骤在 Kubernetes 集群中执行成功。
4.创建集群 cluster2 的 Kubeconfig。
安装 Istio-remote Helm chart 后,在远程集群中创建了一个叫 istio-multi 的 Kubernetes 服务帐号,该服务帐号用于最小化 RBAC 访问请求,对应的集群角色定义如下:
kind:
ClusterRole
apiVersion:
rbac.authorization.k8s.io/v1
metadata:
name: istio-reader
rules:
- apiGroups: ['']
resources: ['nodes', 'pods', 'services',
'endpoints']
verbs: ['get', 'watch', 'list']
下面的过程通过使用先前所述的 istio-multi 服务帐号凭证生成一个远程集群的 kubeconfig 配置文件。通过以下命令,在集群 cluster2 上创建服务帐号 istio-multi 的 Kubeconfig,并保存为文件 n2-k8s-config:
CLUSTER_NAME="cluster2"
SERVER=$(kubectl
config view --minify=true -o "jsonpath={.clusters[].cluster.server}")
SECRET_NAME=$(kubectl
get sa istio-multi -n istio-system -o jsonpath='{.secrets[].name}')
CA_DATA=$(kubectl
get secret ${SECRET_NAME} -n istio-system -o
"jsonpath={.data['ca\.crt']}")
TOKEN=$(kubectl
get secret ${SECRET_NAME} -n istio-system -o
"jsonpath={.data['token']}" | base64 --decode)
cat
<<EOF > n2-k8s-config
apiVersion:
v1
kind:
Config
clusters:
- cluster:
certificate-authority-data: ${CA_DATA}
server: ${SERVER}
name: ${CLUSTER_NAME}
contexts:
- context:
cluster: ${CLUSTER_NAME}
user: ${CLUSTER_NAME}
name: ${CLUSTER_NAME}
current-context:
${CLUSTER_NAME}
users:
- name: ${CLUSTER_NAME}
user:
token: ${TOKEN}
EOF
5.将集群 cluster2 加入 Istio Pilot 所在集群中。
在集群 dusterl 执行以下命令,将上述生成的集群 cluster2 的 kubeconfig 添加到集群 cluster1 的 secret 中。执行这些命令后,集群 cluster1 中的 Istio Pilot 将开始监听集群 cluster2 的服务和实例,就像监听集群 cluster1 中的服务与实例一样:
kubectl
create secret generic n2-k8s-secret --from-file n2-k8s-config -n istio-system
kubectl
label secret n2-k8s-secret istio/multiCluster=true -n istio-system
部署示例应用
为了演示跨集群访问,在第一个 Kubernetes 集群 cluster1 中部署 sleep 应用服务和版本 v1 的 helloworld 服务,在第二个集群 cluster2 中部署版本 v2 的 helloworld 服务,然后验证 sleep 应用是否可以调用本地或者远程集群的 helloworld 服务。
1.部署 sleep 和版本 v1 的 helloworld 服务到第一个集群 cluster1 中,执行如下命令:
kubectl
create namespace app1
kubectl
label namespace app1 istio-injection=enabled
kubectl
apply -n app1 -f multicluster/sleep/sleep.yaml
kubectl
apply -n app1 -f multicluster/helloworld/service.yaml
kubectl
apply -n app1 -f multicluster/helloworld/helloworld.yaml -l version=v1
export
SLEEP_POD=$(kubectl get -n app1 pod -l app=sleep -o
jsonpath={.items..metadata.name})
2.部署版本 v2 的 helloworld 服务到第二个集群 cluster2 中,执行如下命令:
kubectl
create namespace app1
kubectl
label namespace app1 istio-injection=enabled
kubectl
apply -n app1 -f multicluster/helloworld/service.yaml
kubectl
apply -n app1 -f multicluster/helloworld/helloworld.yaml -l version=v2
3.验证在集群 cluster1 中的 sleep 服务是否可以正常调用本地或者远程集群的 helloworld 服务,在集群 cluster1 下执行如下命令:
kubectl
exec $SLEEP_POD -n app1 -c sleep -- curl helloworld.app1:5000/hello
如果设置正确,则在返回的调用结果中可以看到两个版本的 helloworld 服务,同时可以通过查看 sleep 容器组中的 istio-proxy 容器日志来验证访问的端点 IP 地址,返回结果如下所示:
4.验证 Istio 路由规则是否生效。
创建针对上述两个版本的 helloworld 服务的路由规则,以便验证 Istio 配置是否可以正常工作。
创建 Istio 虚拟服务 VirtualService,执行如下命令:
kubectl
apply -n app1 -f multicluster/helloworld/virtualservice.yaml
接着,创建 Istio 目标规则 DestinationRule:
- 如果启用了双向 TLS 通信,则需要如下配置参数:
kubectl
apply -n app1 -f multicluster/helloworld/destinationrule.yaml
- 如果不需要启用双向 TLS 通信,配置参数则需要做出修改,在 YAML 定义中添加trafficPolicy.tls.mode:ISTIO_MUTUAL,定义如下所示:
apiVersion:
networking.istio.io/v1alpha3
kind:
DestinationRule
metadata:
name: helloworld
spec:
host: helloworld
**trafficPolicy:
tls:
mode: ISTIO_MUTUAL**
subsets:
- name: v1
labels:
version: v1
- name: v2
labels:
version: v2
通过执行命令 kubectl apply 创建启用双向 TLS 的 Istio 目标规则,如下所示:
kubectl
apply -n app1 -f multicluster/helloworld/destinationrule-auth.yaml
多次调用 helloworld 服务,只会返回版本 v2 的响应结果,如下所示:
**《Istio服务网格技术解析与实战》读者可免费体验 ASM 产品进行学习!**点击了解阿里云服务网格产品 ASM:www.aliyun.com/product/servicemesh
作者简介
王夕宁 阿里云高级技术专家,阿里云服务网格产品 ASM 及 Istio on Kubernetes 技术负责人,专注于 Kubernetes、云原生、服务网格等领域。曾在 IBM 中国开发中心工作,担任过专利技术评审委员会主席,拥有 40 多项相关领域的国际技术专利。《Istio 服务网格解析与实战》一书由其撰写,详细介绍了 Istio 的基本原理与开发实战,包含大量精选案例和参考代码可以下载,可快速入门 Istio 开发。Gartner 认为,2020 年服务网格将成为所有领先的容器管理系统的标配技术。本书适合所有对微服务和云原生感兴趣的读者,推荐大家对本书进行深入的阅读。
赠书福利
4 月 30 日 10:00 前在阿里巴巴云原生公众号文章留言区写下对阿里云服务网格产品 ASM 的想法或是对服务网格技术 Istio 的疑惑:
- 收获赞最多的前 3 名,将获得《Istio 服务网格技术解析与实践》一书 ;
- 收获赞最多的第 4-6 名,将获得阿里云数据线;
- 收获赞最多的第 7-10 名,将获得 CNCF 官方正版指尖陀螺。
“阿里巴巴云原生关注微服务、Serverless、容器、Service Mesh 等技术领域、聚焦云原生流行技术趋势、云原生大规模的落地实践,做最懂云原生开发者的公众号。”
来源:oschina
链接:https://my.oschina.net/u/3874284/blog/4256469