目录

service作用

kube-proxy三种代理模式

userspace模式

iptables模式

ipvs模式

iptables与ipvs对比

service类型

service参数

service创建

1:ClusterIP类型

ClusterIP Service

命令创建service

通过YAML文件创建service

headless service

DNS

2:NodePort类型

3:LoadBalancer

MetalLB

新版本MetalLB

4:ExternalName

公网域名引入

不同命名空间访问

在pod中使用域名进行访问

sessionAffinity


service作用

使用kubernetes集群运行工作负载时,由于Pod经常处于用后即焚状态,Pod经常被重新生成,因此 Pod对应的IP地址也会经常变化,导致无法直接访问Pod提供的服务,Kuberetes中使用了Service来解决 这一问题,即在Pod前面使用Service对Pod进行代理,无论Pod怎样变化,只要有Label,就可以让 Service能够联系上Pod,把Pod lP地址添加到Service对应的端点列表(Endpoints)实现对Pod IP跟踪, 进而实现通过Service访问Pod目的。

  • 通过service为pod客户端提供访问pod方法,即可客户端访问pod入口

  • 通过标签动态感知podIP地址变化等

  • 防止pod失联

  • 定义访问pod访问策略

  • 通过label-selector相关联

  • 通过Service实现Pod的负载均衡(TCP/UDP4层)

  • 底层实现由kube-proxy通过userspace、iptables、ipvs三种代理模式

kube-proxy三种代理模式

  • kubernetes集群中有三层网络,一类是真实存在的,例如Node Network、Pod Network,提供真实IP地址;一类是虚拟的,例如Cluster Network或Service Network,提供虚拟IP地址,不会出现在接口上,仅会出现在Service当中

  • kube-proxy始终watch(监控)kube-apiserver上关于Service相关的资源变动状态,一旦获取相关信息kube-proxy都要把相关信息转化为当前节点之上的,能够实现Service资源调度到特定Pod之上的规则,进而实现访问Service就能够获取Pod所提供的服务

  • kube-proxy三种代理模式:userspace模式、iptables模式、ipvs模式

userspace模式

userspace 模式是 kube-proxy 使用的第一代模式,该模式在 kubernetes v1.0 版本开始支持使用。

userspace 模式的实现原理图示如下:

kube-proxy会为每个service随机监听一个端口(proxyport),并增加一条iptables规则。所以通过ClusterIP:Port访问Service的报文都redirect到proxy port,kube-proxy从它监听的proxy port收到报文以后,走round robin(默认)或是session affinity(会话亲和力,即同client IP 都走同一链路给同一pod 服务),分发给对应的 pod。

由于 userspace 模式会造成所有报文都走一遍用户态(也就是Service 请求会先从用户空间进入内核 iptables,然后再回到用户空间,由kube-proxy 完成后端 Endpoints 的选择和代理工作),需要在内核 空间和用户空间转换,流量从用户空间进出内核会带来性能损耗,所以这种模式效率低、性能不高,不 推荐使用。

image.png

iptables模式

iptables 模式是 kube-proxy使用的第二代模式,该模式在 kubernetes v1.1版本开始支持,从v1.2 版本开始成为 kube-proxy 的默认模式。

iptables 模式的负载均衡模式是通过底层 netfilter/iptables 规则来实现的,通过 informer 机制 Watch接口实时跟踪 Service 和 Endpoint 的变更事件,并触发对 iptables 规则的同步更新。

iptables 模式的实现原理图示如下:

通过图示可以发现在 iptables模式下,kube proxy只是作为 controller,而不是server,真正服务的是 内核的 netfilter,体现在用户态 的是 iptables。所以整体的效率会比 userspace 模式高。

image.png

ipvs模式

ipvs 模式被 kube-proxy采纳为第三代模式,模式在 kubernetes v1.8 版本开始引入,在 v1.9 版本中处 于 beta 阶段,在 v1.11 版本中正式开始使用。

ipvs(iP Virtual Server)实现了传输层负载均衡,也就是4层交换,作为 Linux 内核的一部分。ipvs运行在 主机上,在真实服务器前充当负载均衡器。ipvs 可以将基于 TCP和 UDP 的服务请求转发到真实服务器 上,并使真实服务器上的服务在单个IP地址上显示为虚拟服务。

ipvs 模式的实现原理图示如下:

image.png

ipvs 和 iptables 都是基于 netfilter 的,那么ipvs 模式有哪些更好的性能呢?

  • ipvs 为大型集群提供了更好的可拓展性和性能

  • ipvs 支持比 iptables 更复杂的负载均衡算法(包括:最小负载、最少连接、加权等)

  • ipvs 支持服务器健康检查和连接重试等功能

  • 可以动态修改 ipset的集合,即使iptables 的规则正在使用这个集合

ipvs 依赖于 iptables。ipvs 会使用 iptables 进行包过滤、airpin-masquerade tricks(地址伪装)、SNAT 等功能,但是使用的是 iptables 的扩展ipset,并不是直接调用 iptables 来生成规则链。通过 ipset 来存 储需要 DROP 或 masquerade 的流量的源或目标地址,用于确保iptables 规则的数量是恒定的,这样我 们就不需要关心有多少 Service 或是 Pod 了。

使用 ipset 相较于 iptables有什么优点呢?iptables 是线性的数据结构,而ipset引入了带索引的数据结 构,当规则很多的时候,ipset 依然可以很高效的查找和匹配。可以将 ipset 简单理解为一个IP(段)的集 合,这个集合的内容可以是IP 地址、IP 网段、端口等,iptables 可以直接添加规则对这个“可变的集合进 行操作”,这样就可以大大减少iptables规则的数量,从而减少性能损耗。

举一个例子,如果我们要禁止成千上万个IP访问我们的服务器,如果使用 iptables 就需要一条一条的添 加规则,这样会在 iptables 中生成大量的规则:如果用ipset 就只需要将相关的IP 地址(网段)加入到 ipset 集合中,然后只需要设置少量的 iptables 规则就可以实现这个目标。

下面的表格是ipvs模式下维护的ipset表集合:

iptables与ipvs对比

iptables

  • 工作在内核空间

  • 优点

    • 灵活,功能强大(可以在数据包不同阶段对包进行操作)
  • 缺点

    • 表中规则过多时,响应变慢,即规则遍历匹配和更新,呈线性延时
  • ipvs

  • 工作在内核空间

  • 优点

    • 转发效率高

    • 调度算法丰富:rr,wrr,lc,wlc,ip hash等

  • 缺点

    • 内核支持不全,低版本内核不能使用,需要升级到4.0或5.0以上。
  • 使用iptables与ipvs时机

  • 1.10版本之前使用iptables(1.1版本之前使用UserSpace进行转发)

  • 1.11版本之后同时支持iptables与ipvs,默认使用ipvs,如果ipvs模块没有加载时,会自动降 级至iptables

service类型

  • ClusterIP

  • 默认,分配一个集群内部可以访问的虚拟IP

  • NodePort

  • 在每个Node上分配一个端口作为外部访问入口

  • nodePort端口范围为:30000-32767

  • LoadBalancer

  • 工作在特定的Cloud Provider上,例如Google Cloud,AWS,OpenStack

  • ExternalName

  • 表示把集群外部的服务引入到集群内部中来,即实现了集群内部pod和集群外部的服务进行通 信

service参数

  • port 访问service使用的端口

  • targetPort 访问 Pod中容器端口

  • nodePort 通过Node实现外网用户访问k8s集群内service(30000-32767)

service创建

Service的创建在工作中有两种方式,一是命令行创建,二是通过资源清单文件YAML文件创建。

1:ClusterIP类型

ClusterlP根据是否生成ClusterlP又可分为普通Service和Headless Service。

service两类:

  • 普通service:

为Kubernetes的Service分配一个集群内部可访问的固定虚拟IP(Cluster IP),实现集群内的访问,。

  • Headless Service

该服务不会分配Cluster IP,也不通过kube-proxy做反向代理和负载均衡。而是通过DNS提供稳定的网络 ID来访问,DNS会将headless service的后端直接解析为pod IP列表。

ClusterIP Service
命令创建service

创建deployment类型的应用

apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx-server1
spec:
  replicas: 2
  selector:
    matchLabels:
      app: nginx
  template:
    metadata:
      labels:
        app: nginx
    spec:
      containers:
      - name: c1
        image: nginx:1.26-alpine
        imagePullPolicy: IfNotPresent
        ports:
        - containerPort: 80

创建clusterIP类型service与Deployment类型应用关联

kubectl expose deployment nginx-server1 --type=ClusterIP --target-port=80 --port=80

说明:

  • expose 创建service

  • deployment.apps 控制器类型

  • nginx-server1 应用名称,也是service名称

  • --type=ClusterIP 指定service类型

  • --target-port=80 指定Pod中容器端口

  • --port=80 指定service端口

kubectl get svc

查看详情描述

kubectl describe service nginx-server1

访问clusterIP就可以看到网页内容

curl http://10.103.196.143

验证负载均衡功能

查看pod

kubectl get pod

修改nginx-server1-5b6d5cd699-mxgc2网页内容为web1

kubectl exec -it nginx-server1-5b6d5cd699-mxgc2 -- /bin/sh
/ # cd /usr/share/nginx/html
/usr/share/nginx/html # ls
50x.html    index.html
/usr/share/nginx/html # echo "web1" > index.html
/usr/share/nginx/html # exit

修改nginx-server1-5b6d5cd699-vtc7b网页内容为web2

kubectl exec -it nginx-server1-5b6d5cd699-vtc7b -- /bin/sh
/ # cd /usr/share/nginx/html
/usr/share/nginx/html # ls
50x.html    index.html
/usr/share/nginx/html # echo "web2" > index.html
/usr/share/nginx/html # exit

验证网页显示

curl http://10.103.196.143

通过YAML文件创建service

编写YAML文件

apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx-server1
spec:
  replicas: 2
  selector:
    matchLabels:
      app: nginx
  template:
    metadata:
      labels:
        app: nginx
    spec:
      containers:
      - name: c1
        image: nginx:1.26-alpine
        imagePullPolicy: IfNotPresent
        ports:
        - containerPort: 80
---
apiVersion: v1
kind: Service
metadata:
  name: nginx-svc
spec:
  type: ClusterIP
  ports:
  - protocol: TCP
    port: 80
    targetPort: 80
  selector:
    app: nginx

应用YAML

kubectl apply -f nginx_deployment.yml

验证

kubectl get svc,pod

kubectl describe service nginx-svc

kubectl get endpoints

curl http://10.108.242.7

headless service
  • 普通的clusterIP service是service name解析为cluster ip,然后cluster ip对应到后面pod ip。

  • headless service是指service name直接解析为后面的pod ip

创建deployment控制器类型的YAML文件

apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx-server1
spec:
  replicas: 2
  selector:
    matchLabels:
      app: nginx
  template:
    metadata:
      labels:
        app: nginx
    spec:
      containers:
      - name: c3
        image: nginx:1.26-alpine
        imagePullPolicy: IfNotPresent
        ports:
        - containerPort: 80

应用YAML文件

kubectl apply -f nginx_deployment.yml

查看

kubectl get pod

创建headless service的YAML文件

apiVersion: v1
kind: Service
metadata:
  name: headless-service
  namespace: default
spec:
  type: ClusterIP    #ClusterIP类型也是默认类型
  clusterIP: None    #None代表无头服务
  ports:    #指定service端口及容器端口
  - port: 80    #service ip中的端口
    protocol: TCP
    targetPort: 80    #pod端口
  selector:    #指定后端pod标签
    app: nginx

应用YAML文件

kubectl apply -f headless-service.yml

查看

kubectl get svc

kubectl get pod -o wide

kubectl get endpoints

DNS

DNS服务监视Kubernetes APl,为每一个Service创建DNS记录用于域名解析

headless service需要DNS来解决访问问题

DNS记录格式为:..svc.cluster.local.

查看kube-dns服务的IP

kubectl get pod -o wide -n kube-system | grep dns

kubectl get svc -n kube-system | grep dns

在集群主机通过DNS服务地址查找无头服务的dns解析

dig -t a headless-service.default.svc.cluster.local. @10.96.0.10

在集群内创建pod进行解析

kubectl run -it centos --image=centos:7 --image-pull-policy=IfNotPresent
curl http://headless-service.default.svc.cluster.local.

2:NodePort类型

创建YAML文件

apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx-app
  labels:
    app: nginx-app
spec:
  replicas: 2
  selector:
    matchLabels:
      app: nginx-app
  template:
    metadata:
      labels:
        app: nginx-app
    spec:
      containers:
      - name: c2
        image: nginx:1.26-alpine
        imagePullPolicy: IfNotPresent
        ports:
        - containerPort: 80
---
apiVersion: v1
kind: Service
metadata:
  name: nginx-app
spec:
  type: NodePort
  ports:
  - protocol: TCP
    nodePort: 31111
    port: 8060
    targetPort: 80
  selector:
    app: nginx-app

应用YAML文件

kubectl apply -f nginx_nodeport.yml

查看

kubectl get svc,pod
kubectl get deployment

直接使用浏览器访问

http://192.168.18.128:31111/

http://192.168.18.129:31111/

3:LoadBalancer

集群外访问过程

  1. 用户

  2. 域名

  3. 云服务提供商提供LB服务

  4. NodeIP:Port(service IP)

  5. Pod IP:端口

MetalLB

自建kubernetes的loadbalancer类型服务方案

MetalLB可以为kubernetes集群中的Service提供网络负载均衡功能

MetalLB两大功能为:

  • 地址分配,类似于DHCP

  • 外部通告,一旦MetalLB为服务分配了外部IP地址,它就需要使群集之外的网络意识到该IP在群集 中"存在"。MetalLB使用标准路由协议来实现此目的:ARP,NDP或BGP。

下载清单资源

wget https://raw.githubusercontent.com/metallb/metallb/v0.12.1/manifests/namespace.yaml
wget https://raw.githubusercontent.com/metallb/metallb/v0.12.1/manifests/metallb.yaml

应用文件

kubectl apply -f namespace.yaml
kubectl apply -f metallb.yaml

查看

kubectl get ns
kubectl get pod -n metallb-system

问题解决:如果出现pod中有失败状态,可以在node节点中直接导入镜像文件(metallbcontroller.tar 和metallbspeaker.tar),解决镜像下载问题

可以给metallb做资源配置

kubectl get configmap -n metallb-system

准备metallb配置文件

apiVersion: v1
kind: ConfigMap
metadata:
  namespace: metallb-system
  name: config
data:
  config: |          #定义了一个名为 config 的配置项,| 符号表示保留后续内容的格式
    address-pools:
    - name: default
      protocol: layer2
      addresses:
      - 192.168.18.100-192.168.18.200    #与集群节点服务器处于同一网段

应用yaml文件

kubectl apply -f metallb-conf.yaml

查看

kubectl get configmap -n metallb-system

kubectl describe cm config -n metallb-system

创建nginx应用资源

apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx-metallb
spec:
  replicas: 2
  selector:
    matchLabels:
      app: nginx
  template:
    metadata:
      labels:
        app: nginx
    spec:
      containers:
      - name: nginx-metallb1
        image: nginx:1.26-alpine
        imagePullPolicy: IfNotPresent
        ports:
        - containerPort: 80

应用yaml文件

kubectl apply -f nginx-metallb.yaml

查看

kubectl get deployment
kubectl get pod

创建service资源

apiVersion: v1
kind: Service
metadata:
  name: nginx-metallb
spec:
  type: LoadBalancer
  ports:
  - port: 80
    protocol: TCP
    targetPort: 80
  selector:
    app: nginx

应用yaml

kubectl apply -f lb-service.yaml

查看

kubectl get svc

访问地址:http://192.168.18.100/

使用集群节点IP也可以访问,此时端口号为31759

新版本MetalLB

1:修改kube-proxy配置文件

kubectl edit configmap -n kube-system kube-proxy
#修改41行
apiVersion: kubeproxy.config.k8s.io/v1alpha1
kind: KubeProxyConfiguration
mode: "ipvs"     #检查模式
ipvs:
strictARP: true     #设置为true

2:使用YAML文件创建资源(需要科学上网)

kubectl apply -f https://raw.githubusercontent.com/metallb/metallb/v0.15.2/config/manifests/metallb-native.yaml

同时也可以把YAML下载

wget https://raw.githubusercontent.com/metallb/metallb/v0.15.2/config/manifests/metallb-native.yaml

3:查看创建资源

kubectl get all -n metallb-system

打开官网配置

这里就和之前版本有区别

4:不需要创建configmap资源对象,而是直接使用IPAddressPool资源

apiVersion: metallb.io/v1beta1
kind: IPAddressPool
metadata:
  name: first-pool
  namespace: metallb-system
spec:
  addresses:
  - 192.168.18.200-192.168.18.209

5:应用、查看

kubectl apply -f ipaddresspool.yaml
kubectl get ipaddresspool -n metallb-system

6:创建nginx应用资源

apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx-metallb
spec:
  replicas: 2
  selector:
    matchLabels:
      app: nginx
  template:
    metadata:
      labels:
        app: nginx
    spec:
      containers:
      - name: nginx-metallb1
        image: nginx:1.26-alpine
        imagePullPolicy: IfNotPresent
        ports:
        - containerPort: 80

应用、查看

kubectl apply -f nginx-metallb.yaml
kubectl get deployment
kubectl get pod

7:创建service资源

apiVersion: v1
kind: Service
metadata:
  name: nginx-metallb
spec:
  type: LoadBalancer
  ports:
  - port: 80
    protocol: TCP
    targetPort: 80
  selector:
    app: nginx
kubectl apply -f lb-service.yaml
kubectl get svc

验证查看网站

4:ExternalName

作用:

  • 把集群外部的服务引入到集群内部中来,实现了集群内部pod和集群外部的服务进行通信

  • ExternalName类型的服务适用于外部服务使用域名的方式,缺点是不能指定端口

  • 还有一点要注意:集群内的Pod会继承Node上的DNS解析规则。所以只要Node可以访问的服务, Pod中也可以访问到,这就实现了集群内服务访问集群外服务

公网域名引入

查看dns资源

kubectl get svc -n kube-system | grep dns

查看公网域名解析

dig -t a www.baidu.com @10.96.0.10

创建YAML

vim externelname.yml
#写入
apiVersion: v1
kind: Service
metadata:
  name: my-externalname
  namespace: default
spec:
  type: ExternalName
  externalName: www.baidu.com

应用YAML

kubectl apply -f externelname.yml

查看创建资源

kubectl get svc | grep exter

查看本地域名解析

dig -t A my-externalname.default.svc.cluster.local. @10.96.0.10

开启测试Pod来解析域名

kubectl run -it expod --image=busybox:1.28
/ # nslookup www.baidu.com        #输入公网域名
/ # nslookup my-externalname.default.svc.cluster.local.      #输入本地域名

不同命名空间访问

案例:实现ns1和ns2两个命名空间之间服务的访问

1:创建ns1命名空间和相关deployment,pod,service

apiVersion: v1
kind: Namespace
metadata:
  name: ns1
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: deploy-nginx
  namespace: ns1
spec:
  replicas: 1
  selector:
    matchLabels:
      app: nginx
  template:
    metadata:
      labels:
        app: nginx
    spec:
      containers:
      - name: nginx
        image: nginx:1.26-alpine
        imagePullPolicy: IfNotPresent
        ports:
        - containerPort: 80
---
apiVersion: v1
kind: Service
metadata:
  name: svc1
  namespace: ns1    #服务名称
spec:               #属于ns1命名空间
  selector:
    app: nginx
  clusterIP: None   #无头服务
  ports:
  - port: 80
    targetPort: 80
    protocol: TCP
---
apiVersion: v1
kind: Service
metadata:
  name: external-svc1
  namespace: ns1    #属于ns1命名空间
spec:
  type: ExternalName
  externalName: svc2.ns2.svc.cluster.local    #将ns2空间的svc2服务引入到ns1空间

2:应用YAML

kubectl apply -f ns1-nginx.yaml

3:查看命名空间ns1中的资源

kubectl get pods,svc -n ns1

4:使用dns:10.96.0.10解析域名,可以直接看到pod的ip

kubectl get svc -n kube-system
dig -t a svc1.ns1.svc.cluster.local. @10.96.0.10
kubectl get pods -n ns1 -o wide

5:创建ns2命名空间和相关deployment,pod,service

apiVersion: v1
kind: Namespace
metadata:
  name: ns2
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: deploy-nginx
  namespace: ns2
spec:
  replicas: 1
  selector:
    matchLabels:
      app: nginx
  template:
    metadata:
      labels:
        app: nginx
    spec:
      containers:
      - name: nginx
        image: nginx:1.26-alpine
        imagePullPolicy: IfNotPresent
        ports:
        - containerPort: 80
---
apiVersion: v1
kind: Service
metadata:
  name: svc2
  namespace: ns2
spec:
  selector:
    app: nginx
  clusterIP: None
  ports:
  - port: 80
    protocol: TCP
    targetPort: 80
---
apiVersion: v1
kind: Service
metadata:
  name: external-svc2
  namespace: ns2
spec:
  type: ExternalName
  externalName: svc1.ns1.svc.cluster.local

应用、查看

kubectl apply -f ns2-nginx.yaml
kubectl get pods,svc -n ns2

7:使用dns解析pod的ip

dig -t a svc2.ns2.svc.cluster.local. @10.96.0.10

kubectl get pods -n ns2 -o wide

在pod中使用域名进行访问

进入ns1中的pod,使用nslookup对ns2中的pod进行域名解析

kubectl exec -it deploy-nginx-66d785bdb5-2lwq6 -n ns1 -- /bin/sh
/ # nslookup svc1
/ # nslookup svc2.ns2.svc.cluster.local.

如果ns2中pod的ip发生变化,那么是否还能够域名正常解析

先查看ns2中pod的ip

kubectl get pods -n ns2 -o wide

再删除pod,并查看ip变化为10.244.166.138

kubectl delete pod deploy-nginx-66d785bdb5-qzw62 -n ns2
kubectl get pods -n ns2 -o wide

再次进入到ns1中pod,对ns2中pod进行域名解析

kubectl exec -it deploy-nginx-66d785bdb5-2lwq6 -n ns1 -- /bin/sh
/ # nslookup svc2.ns2.svc.cluster.local.

sessionAffinity

会话粘黏

设置sessionAffinity为clientip(类似nginx的ip_hash算法、lvs的sh算法)

创建nginx资源使用clusterip访问

apiVersion: apps/v1
kind: Deployment
metadata:
  name: deploy-nginx
spec:
  replicas: 2
  selector:
    matchLabels:
      app: nginx
  template:
    metadata:
      labels:
        app: nginx
    spec:
      containers:
      - name: c1
        image: nginx:1.26-alpine
        imagePullPolicy: IfNotPresent
        ports:
        - containerPort: 80
---
apiVersion: v1
kind: Service
metadata:
  name: nginx-svc
spec:
  type: ClusterIP
  ports:
  - port: 80
    targetPort: 80
    protocol: TCP
  selector:
    app: nginx

应用YAML、查看资源

kubectl apply -f deployment-nginx-svc.yaml
kubectl get pods,svc

更新两个pod中首页内容作为鉴别区分

修改第1个pod首页内容为web1

kubectl exec -it deploy-nginx-5b6d5cd699-4sf5j -- /bin/sh
/ # cd /usr/share/nginx/html/
/usr/share/nginx/html # ls
50x.html    index.html
/usr/share/nginx/html # echo "web1" > index.html
/usr/share/nginx/html # exit

修改第2个pod首页内容为web2

kubectl exec -it deploy-nginx-5b6d5cd699-st275 -- /bin/sh
/ # cd /usr/share/nginx/html/
/usr/share/nginx/html # ls
50x.html    index.html
/usr/share/nginx/html # echo "web2" > index.html
/usr/share/nginx/html # exit

可以直接观测到访问clusterip使用了负载均衡

curl http://10.96.194.1
web1
curl http://10.96.194.1
web2
kubectl describe svc nginx-svc

kubectl patch svc nginx-svc -p '{"spec":{"sessionAffinity":"ClientIP"}}'

再次查看更改结果

kubectl describe svc nginx-svc

验证访问粘黏,第1次访问哪个pod,后面就一直访问这个pod,直到失效时间

sessionAffinity机制默认失效时间为10800秒(3小时)

curl http://10.96.194.1
Logo

开源鸿蒙跨平台开发社区汇聚开发者与厂商,共建“一次开发,多端部署”的开源生态,致力于降低跨端开发门槛,推动万物智联创新。

更多推荐