Istio 南北向流量管理

引言

之前的文章:Istio 东西向流量管理
这次讲的是南北向流量管理,也就是入口请求到集群服务的流量管理

Istio 网关

网络社区中有一个术语叫Ingress,是指入口请求到集群内服务的流量管理。Ingress指的是本地网络之外的流量请求本地集群网络中的端口。此流量先路由公开的入口点,以便通过执行一些本地网络规则和策略来确认哪些流量被允许进入。如果流量未通过这些入口点,则无法与集群的任何服务连接。如果入口点允许流量进入,则将其代理到本地网络中合适的节点Istio入口流量的管理是由Istio网关进行的。

Istio网关的工作原理

传统上,Kubernetes使用Ingress控制器来处理外部进入集群的流量。使用Istio时,Istio网关用新的Gateway资源和VirtualService资源来控制入口流量,它们协同工作将流量路由到网格中。在网格内部不需要Gateway,因为服务可以通过集群本地服务名称互相访问(DNS服务发现)

  1. 客户端在特定端口上发出请求,例如:http://test.com:38081/,这是外部请求。
  2. 负载均衡器在这个端口上进行侦听,并将请求转发到集群中
  3. 在集群内部,请求被路由到Istio IngressGateway服务所侦听的负载均衡器转发的端口上。
  4. Istio IngressGateway 服务将请求转发到对应的pod上。
  5. IngressGateway pod上会配置Gateway资源VirtualService资源定义。Gateway会配置端口、协议及相关安全证书VirtualService路由配置信息用于找到正确的服务(在东西向流量管理文章中我们已经讲过路由配置了)。
  6. Istio IngressGateway pod会根据路由配置信息将请求路由到对应的服务上。
  7. 应用服务将请求路由到对应应用pod中的实例上。

Istio 网关负载均衡器的作用

典型的服务网格具有一个多个负载均衡器,也称为网关(Gateway),它们从外部网络终止TLS并允许流量进入网格。然后,流量通过边车网关(Sidecar Gateway)流经内部服务。
应用程序使用外部服务的场景也很常见,也可以直接调用外部服务,或者在某些部署中强制通过专用出口网关(Egress Gateway)管理离开网格的所有流量。

Istio具有入口网关的概念,它扮演网络入口点的角色,负责保护和控制来自集群外部的流量对集群内的访问
此外,Istio网关还扮演负载均衡虚拟主机路由的角色。之前提到过的Envoy,是一个功能强大的服务到服务代理,但它也有负载均衡路由的功能,可代理的流量包括从服务网格外部到其内部运行的服务,或者从集群内部服务到外部的服务

入口网关(IngressGateway)服务

IngressGateway服务必须监听所需的端口,以便能够将流量转发到IngressGateway pod上。minikube + Istio 1.7.4 部署demo项目这文章中简单的介绍过。

这里是看一眼IngressGateway常用的端口,注意这里只显示了port外部访问用的nodePort白话理解就是IngressGateway中的端口直接访问没有什么用处,因为实际上它selectorpod中的containeristio-proxy,它需要与VirtualServiceGateway结合起来,让集群内知道从外部访问的流量如何被路由到内部服务网格:

ports:
- name: http2
  nodePort: 30000
  port: 80
  protocol: TCP
- name: https
  nodePort: 30443
  port: 443
  protocol: TCP
- name: mysqlS
  nodePort: 30306
  port: 3306
  protocol: TCP

入口网关部署

当我们创建或更改一个GatewayVirtualService时,Istiod会检测到这些变更,并将这些变更信息转换为Envoy配置,然后将Envoy配置信息发送给相关Envoy代理,包括内部的EnvoyIngressGateway中的Envoy
注意:请不要混淆IngressGateway和Gateway,Gateway资源只是用于配置IngressGateway的一种Istio的自定义资源。

网关资源

网关(Gateway)资源用来配置Envoy端口,前面我们看了IngressGateway中常用的端口,这些端口是外部流量通过这些端口流入,我们可以让这些流量和网格内的服务互通,记得最早的文章说过,服务网格Sidecar只能是在网格内的流量才会使用到Envoy,所以这里需要IngressGateway让外部流量进入服务网格。

例子:使用Gateway

这里跳过了Service和Deployment的创建,我们可知的是Service绑定了80端口,服务名是test,绑定了Deployment中的一个web。

创建一个Gateway

apiVersion: networking.istio.io/v1alpha3
kind: Gateway
metadata:
  name: test-gateway
  namespace: istio-system
spec:
  selector: # 这里就是使用了istio-ingressgateway
    istio: ingressgateway
  servers:
  - hosts: # 可以配置多个host,这里使用通配符不限制host
    - "*"
    port:
      name: http
      number: 80
      protocol: HTTP
  - hosts:
    - "*"
    port:
      name: https
      number: 443
      protocol: HTTPS
    tls:
      mode: SIMPLE
      # 除了使用privateKey和serverCertificate指定挂载路径也可以直接使用credentialName来指定secret名
      # credentialName: istio-ingressgateway-certs
      privateKey: /etc/istio/ingressgateway-certs/tls.key
      serverCertificate: /etc/istio/ingressgateway-certs/tls.crt

VirtualService使用Gateway

apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
  name: test-vs
spec:
  gateways: # 可以写很多gateway,我们只使用刚才创建的即可。
  - test-gateway.istio-system.svc.cluster.local # 因为命名空间的关系,我们写全了服务名,也可以简写,这里为了方便萌新理解,写全了。
  # 之前的文章host我们直接写服务名,因为DNS服务发现可以帮助我们直接解析集群内部服务host,这次我们讲的是外部流量,这里的host写的是真实外网域名哦。
  hosts:
  - test.example.com
  http:
  - match:
    - uri:
      prefix: /
    route:
    - destination:
        host: test # 这里的host是内部服务名,通过DNS服务发现即可解析
        port:
          number: 80

我们理一下过程

  1. 外部通过test.example.com:NodePort访问,流量先是进入IngressGateway,被转发到内部80端口。
  2. 通过test-gateway中的servers,我们知道IngressGateway80端口对应到test-gatewayhttp服务,此时流量就已经算是进入网格了。然后VirtualService根据test-gatewayhost做匹配然后配合路由规则,流量被转发到test服务。

使用HTTPS协议

目前来说,有部分CDN提供免费/收费HTTPS服务,对于CDN来说(反向代理+智能DNS解析),使用HTTPS分为2种,一种是Flexible,另一种是Full

  • Flexible模式是这样,仅是用户←→CDN→后端(注意CDN到后端的请求会变为HTTP)
  • Full模式是这样:用户←→CDN←→后端

如果为了方便我们可以选择Flexible模式,这样我们后端仅使用HTTP即可,但是对于用户来说是HTTPS
这里要说的是如果使用Full模式,我们就必须在后端也使用HTTPS,且将证书上传到CDN

这里的情况就很像上面描述的方式,我们来说说Istio中如何使用HTTPS

创建TLS Secret

创建的secret必须在istio-systemnamespace,并且命名也是固定的(istio-ingressgateway-certs),否则无法在Istio Gateway中使用。

关于tlssecret的名称为什么是固定的,其实在istio-ingressgatewaypod中就能看见,我选了关键的几段。
注意:看挂载路径,再回头看我们之前gateway中配置的私钥路径和证书路径。

 containers:
 - name: istio-proxy
   image: "docker.io/istio/proxyv2:1.1.0"
   imagePullPolicy: IfNotPresent
   volumeMounts:
   - name: istio-certs
     mountPath: /etc/certs
     readOnly: true
   - name: ingressgateway-certs
     mountPath: "/etc/istio/ingressgateway-certs"
     readOnly: true
   - name: ingressgateway-ca-certs
     mountPath: "/etc/istio/ingressgateway-ca-certs"
     readOnly: true
volumes:
- name: istio-certs
  secret:
    secretName: istio.istio-ingressgateway-service-account
    optional: true
- name: ingressgateway-certs
  secret:
    secretName: "istio-ingressgateway-certs"
    optional: true
- name: ingressgateway-ca-certs
  secret:
    secretName: "istio-ingressgateway-ca-certs"
    optional: true

Secret用来保存证书和私钥

kubectl create -n istio-system secret tls istio-ingressgateway-certs --key httpbin.example.com/private/httpbin.example.com.key.pem --cert httpbin.example.com/certs/httpbin.example.com.cert.pem

使用HTTPS

我们之前在test-gateway中已经配置过443端口的HTTPS,只是当时没有key和证书,现在我们创建完了secret,就可以使用它了。

创建Deployment和Service

apiVersion: extensions/v1beta1
kind: Deployment
metadata:
  name: httpbin
  labels:
    app: httpbin
spec:
  replicas: 1
  template:
    metadata:
      labels:
        app: httpbin
        version: v1
  spec:
    containers:
    - image: docker.io/citizenstig/httpbin
      imagePullPolicy: IfNotPresent
      name: httpbin
      prots:
      - containerPort: 8080
---
apiVersion: v1
kind: Service
metadata: httpbin
spec: 
  ports:
  - name: http
    port: 8080
  selector:
    app: httpbin


# ***创建VirtualService***

apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
  name: httpbin-vs
spec:
  gateways: # gateway没变
  - test-gateway.istio-system.svc.cluster.local 
  hosts: # 为了区分 我们新建了一个VirtualService并且HOST也不同了
  - httpbin.example.com
  http: # 网关在接收到https请求后我们会转为http转发到后端
  - match:
    - uri:
      prefix: /
    route:
    - destination:
        host: httpbin # 这里的host是内部服务名,通过DNS服务发现即可解析
        port:
          number: 8080

为多个主机配置TLS入口网关

之前的yaml注释中写了
除了使用privateKeyserverCertificate指定挂载路径也可以直接使用credentialName来指定secret名,这样我们不通过修改或添加ingress-gateway的挂载也可以直接使用新的tls secret
Gateway资源清单中通过不同的host使用不同的证书。
但是此时VirtualService就需要分开建立,因为需要每个host对应一个VirtualService

例子:

先创建一个名为nginx-certstls secret

kubectl create -n istio-system secret tls nginx-certs --key nginx-certs/private/nginx.example.com.key.pem --cert nginx-certs/certs/nginx.example.com.cert.pem

修改gateway资源

apiVersion: networking.istio.io/v1alpha3
kind: Gateway
metadata:
  name: test-gateway
  namespace: istio-system
spec:
  selector: # 这里就是使用了istio-ingressgateway
    istio: ingressgateway
  servers:
  - hosts: # 可以配置多个host,这里使用通配符不限制host
    - "*"
    port:
      name: http
      number: 80
      protocol: HTTP
  - hosts:
    # 这里之前是* 因为我们就一个域名,所以不需要限制
    # 现在改为了固定的httpbin.example.com 使用之前的证书
    - httpbin.example.com
    port:
      name: https
      number: 443
      protocol: HTTPS
    tls:
      mode: SIMPLE
      # 除了使用privateKey和serverCertificate指定挂载路径也可以直接使用credentialName来指定secret名
      # credentialName: istio-ingressgateway-certs
      privateKey: /etc/istio/ingressgateway-certs/tls.key
      serverCertificate: /etc/istio/ingressgateway-certs/tls.crt
  - hosts:
    # 这里是使用nginx的证书 域名不同哦
    - nginx.test-nginx.com
    port:
      name: https
      number: 443
      protocol: HTTPS
    tls:
      mode: SIMPLE
      # 除了使用privateKey和serverCertificate指定挂载路径也可以直接使用credentialName来指定secret名
      credentialName: nginx-certs

新建一个nginx的VirtualService

apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
  name: nginx-vs
spec:
  gateways: # gateway没变
  - test-gateway.istio-system.svc.cluster.local 
  hosts: # 注意我们区分了hosts
  - nginx.test-nginx.com
  http: # 网关在接收到https请求后我们会转为http转发到后端
  - match:
    - uri:
      prefix: /
    route:
    - destination:
        host: nginx
        port:
          number: 80

注意:我跳过了Nginx的Deployment和Service的创建。


关于基于SNI的HTTPS路由

SNI的意思是Server Name Indication,客户端发出SSL请求中的ClientHello阶段),就提交Host信息,使得服务器能够切换到正确的域并返回相应的证书。
它的作用是可以在相同的IP使用不同的证书,比如www.test.com和www.test2.com各使用了一个证书,如果没有SNI支持,那么无论使用哪个域名访问,只会使用其中一个证书。

我们如果不使用SNI路由,那么在建立VirtualService时就需要几个域名就建几个VirtualService(因为大多数情况我们的match中的prefix是/,如果我们的hosts同时写了这些域名,我们只能靠match来区分了)

为了区分,我们重建一个Gateway

spec.servers[].tls.mode我们改为了PASSTHROUGH,意义在于 我们不在网关这里使用证书了,表示按照原样传递入口流量而不终止TLS,也就是说进入这个网关的HTTPS流量,我们原样传到后端,这就需要在容器中去配置TLS证书,我们需要配置到nginx里。

关于在nginx里配置证书我这里跳过了,不然yaml太多了。

apiVersion: networking.istio.io/v1alpha3
kind: Gateway
metadata:
  name: sni-gateway
  namespace: istio-system
spec:
  selector: # 这里就是使用了istio-ingressgateway
    istio: ingressgateway
  servers:
  # 之前我们是写了hosts,分别不同的域名用了不同的证书,现在我们写在一起,因为我们的tls mode变了
  - hosts:
    - "nginx.test1-nginx.com"
    - "nginx.test2-nginx.com"
    port:
      name: https
      number: 443
      protocol: HTTPS
    tls:
      mode: PASSTHROUGH

VirtualService使用sni中的host转发到后端

apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata: 
  name: nginx-sni-test
spec:
  hosts:
  - nginx.test1-nginx.com
  - nginx.test2-nginx.com
  gateways:
    - sni-gateway.istio-system.svc.cluster.local
  tls: # 之前我们使用http原因是我们只在网关层使用TLS协议
  - match:
    - sni_hosts:
      - nginx.test1-nginx.com
      port: 443
    route:
    - destination:
        host: nginx1
        port:
          number: 443
  - match:
    - sni_hosts:
      - nginx.test2-nginx.com
      port: 443
    route:
    - destination:
        host: nginx2
        port: 
          number: 443
    
      

出口流量管理

出口流量网关

出口网关使用起来其实也很简单,定义一个VirtualService引导对目标的访问转发到我们建立的egressgateway即可,再引导对egressgateway的访问,我们转发到具体的目标地址

我们现在使用Egress Gatway进行HTTPS流量透传(这是由应用程序发起的TLS)。
为了简单一些,我们不开启TLS双向认证(因为双向认证我会放在安全篇写),直接进行透传即可,我们配置一个出口网关。

apiVersion: networking.istio.io/v1alpha3
kind: Gateway
metadata:
  name: istio-egressgateway
spec:
  selector:
    istio: egressgateway
  servers:
  - port:
      number: 80
      name: http
      protocol: http
    hosts:
    - istio.io
  - port:
      number: 443
      name: https
      protocol: TLS
    hosts:
    - istio.io
    tls:
      mode: PASSTHROUGH
---
apiVersion: networking.istio.io/v1alpha3
kind: DestinationRule
metadata:
  host: istio-egressgateway.istio-system.svc.cluster.local
  subsets:
  - name: test # 因为流策默认是轮训 ,我们可以不显式的写出来

定义一个VirtualService,配置对istio.io的访问80是HTTP协议,443为TLS协议。
如果是网格内对外发出的流量(80和443),会被转发到istio-egressgateway
然后判断如果是istio-egressgateway再对外发出,就会被转发到istio.io本身的域名地址。

apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
  name: direct-through-egress-gateway
spec:
  hosts:
  - istio.io
  gateways:
  - istio-egressgateway
  - mesh
  http:
  - match:
    - gateways:
      - mesh
      port: 80
    route:
    - destination:
        host: istio-egressgateway.istio-system.svc.cluster.local
        subset: test
        port:
          number: 80
      weight: 100
  - match:
    - gateways:
      - istio-egressgateway
      port: 80
    route:
    - destination:
        host: istio.io
        port:
          number: 80
      weight: 100
  tls:
  - match:
    - gateways:
      - mesh
      port: 443
      sni_hosts:
      - istio.io
    route:
    - destination:
        host: istio-egressgateway.istio-system.svc.cluster.local
        subset: test
        port:
          number: 443
  - match:
    - gateways:
      - istio-egressgateway
      port: 443
      sni_hosts:
      - istio.io
    route:
    - destination:
        host: istio.io
        port:
          number: 443
      weight: 100    

Service Entry(服务入口)

王夕宁的书中他写的是服务条目,可能是因为ServiceEntry允许将其他条目添加到Istio内部服务注册表中。

字段类型描述必须的
hostsstring[]与ServiceEntry关联的主机。可以是带有通配符前缀的DNS名称。
主机字段用于在VirtualServices和DestinationRules中选择匹配的主机。
对于HTTP通信,HTTP Host / Authority标头将与hosts字段匹配。
对于包含服务器名称指示(SNI)的HTTP或TLS流量,SNI值将与主机字段匹配。
注意1:当resolution设置为DNS类型且未指定端点时,主机字段将用作将流量路由到的端点的DNS名称。
注意2:如果主机名与另一个服务注册表(例如Kubernetes)中的服务名称匹配,该服务注册表也提供自己的一组端点,则ServiceEntry将被视为现有Kubernetes服务的装饰器。如果适用,服务条目中的属性将添加到Kubernetes服务。当前,将仅考虑以下附加属性istiod
subjectAltNames:除了验证与服务Pod相关联的服务帐户的SAN外,还将验证此处指定的SAN。
addressesstring[]与服务关联的虚拟IP地址。可以是CIDR前缀。HTTP通信,生成的路线的配置将包括两个HTTP路由域addresses和hosts字段值和目的地将基于HTTP Host / Authority标头进行标识。如果指定了一个或多个IP地址,并且如果目标IP与地址字段中指定的IP / CIDR匹配,则传入的流量将被标识为属于该服务。如果“地址”字段为空,则仅根据目标端口识别流量。在这种情况下,网格中的任何其他服务都不得共享正在访问服务的端口。换句话说,Sidecar将充当简单的TCP代理,将指定端口上的传入流量转发到指定的目标端点IP /主机。该字段不支持Unix域套接字地址。
portsPort[]与外部服务关联的端口。如果端点是Unix域套接字地址,则必须只有一个端口。
locationLocation指定是将服务视为网格外部还是网格的一部分。
resolutionResolution主机的服务发现模式。对于没有随附IP地址的TCP端口,将resolution模式设置为NONE时必须小心。在这种情况下,将允许到该端口上任何IP的流量(即0.0.0.0:)。
endpointsWorkloadEntry[]与服务关联的一个或多个端点。只能指定endpoints或之一 workloadSelector。
workloadSelectorWorkloadSelector仅适用于MESH_INTERNAL服务。只能指定endpoints或之一 workloadSelector。WorkloadEntry根据标签选择一个或多个Kubernetes Pod或VM工作负载(使用指定 )。WorkloadEntry代表VM的对象应在与ServiceEntry相同的名称空间中定义。
exportTostring[]此服务导出到的名称空间列表。导出服务允许它被其他名称空间中定义的边车,网关和虚拟服务使用。此功能为服务所有者和网格管理员提供了一种机制,可以控制跨名称空间边界的服务的可见性。
如果未指定名称空间,则默认情况下会将服务导出到所有名称空间。
价值 ”。” 保留,并定义导出到声明服务的同一名称空间。类似地,值“ *”保留并定义到所有命名空间的导出。
对于Kubernetes服务,可以通过将注释“ networking.istio.io/exportTo”设置为以逗号分隔的名称空间名称列表来实现等效效果。
注意:在当前版本中,该exportTo值限制为“.” 或“ *”(即当前名称空间或所有名称空间)。
subjectAltNamesstring[]如果指定,则代理将验证服务器证书的使用者备用名称是否与指定值之一匹配。
注意:当将工作负载条目与工作负载选择器一起使用时,在工作条目中指定的服务帐户也将用于派生应验证的其他使用者备用名称。

Location

名称描述
MESH_EXTERNAL表示服务在网格外部。通常用于指示通过API使用的外部服务。
MESH_INTERNAL表示服务是网格的一部分。通常用于指示在扩展服务网格以包括非托管基础架构的过程中显式添加的服务(例如,添加到基于Kubernetes的服务网格的VM)。

Resolution

名称描述
NONE假定传入连接已经解析(到特定的目标IP地址)。通常使用诸如IP表REDIRECT / eBPF之类的机制通过代理路由此类连接。在执行与路由相关的任何转换之后,代理将把连接转发到连接绑定到的IP地址。
STATIC使用端点中指定的静态IP地址(请参见下文)作为与服务关联的后备实例。
DNS在请求处理期间,尝试通过查询环境DNS来解析IP地址。如果未指定端点,则代理将解析未使用通配符的主机字段中指定的DNS地址。如果指定了端点,则将解析端点中指定的DNS地址,以确定目标IP地址。DNS解析不能与Unix域套接字端点一起使用。

举个栗子:

apiVersion: networking.istio.io/v1alpha3
kind: ServiceEntry
metadata:
  name: external-svc-https
spec:
  hosts:
  - api.dropboxapi.com
  - www.googleapis.com
  - api.facebook.com
  location: MESH_EXTERNAL
  ports:
  - number: 443
    name: https
    protocol: TLS
  resolution: DNS # 如果访问上面3个host(属于外部服务),使用dns解析

再来个栗子:

apiVersion: networking.istio.io/v1alpha3
kind: ServiceEntry
metadata:
  name: external-svc-mongocluster
spec:
  hosts:
  - mymongodb.somedomain # 因为我们不是HTTP服务,实际上hosts中的dns名称会被忽略,这种情况下会使用endpoints中的address以及ports来甄别服务调用
  addresses:
  - 172.20.10.1/24 # 在列表范围内的IP的访问会被认定属于这一服务,因为hosts会被忽略,如果我们不定义address,那么服务甄别就只能靠下面的ports了
  ports:
  - number: 27018
    name: mongodb
    protocol: MONGO
  location: MESH_INTERNAL # 表示服务属于网格内部
  resolution: STATIC # 静态方式解析
  endpoints: # 一个或多个关联到这个服务的endpoint
  - address: 2.2.2.2
  - address: 3.3.3.3

HTTP流量强制升级为TLS

apiVersion: networking.istio.io/v1alpha3
kind: ServiceEntry
metadata:
  name: istio-io
spec:
  hosts:
  - istio.io
  ports:
  - number: 80
    name: http
    protocol: HTTP
  - number: 443
    name: https
    protocol: HTTPS
  resolution: DNS
---
apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
  name: istio-io
spec:
  hosts:
  - istio.io
  http:
  - match:
    - port: 80 # 如果对istio.io访问80端口
      route:
      - destination: # 将它转发到443端口,但是注意,此时协议还是HTTP
          host: istio.io
          port: 443
        weight: 100
---
apiVersion: networking.istio.io/v1alpha3
kind: DestinationRule
metadata:
  name: istio-io
spec:
  host: istio.io
  trafficPolicy:
    loadBanlancer:
      simple: ROUND_ROBIN
    portLevelSettings:
    - port:
        number: 80 # 目标规则定义了80端口
        tls: SIMPLE # 发起到上游端点的TLS连接,也就是说我们是通过流量策略来将对istio.io 80端口的HTTP协议升级成TLS协议。

解释:我们对http://istio.io进行访问,VirtualService将端口80上的请求重定向到目标端口443,在此之前DestinationRule将对端口80上的HTTP请求执行TLS origination,因此我们访问http://istio.io直接就会返回200,不会再进行重定向后跳转,只需要访问一次即可。

关于DestinationRule.trafficPolicy的东西,会在Istio流量治理中细说。

Service Entry Istio官方文档

Egress TLS Origination

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

没事干写博客玩

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值