玩K8S不得不会的HELM

举报
kaliarch 发表于 2019/10/29 12:25:10 2019/10/29
【摘要】 一 基本概念helm 类似于Linux系统下的包管理器,如yum/apt等,可以方便快捷的将之前打包好的yaml文件快速部署进kubernetes内,方便管理维护。helm:一个命令行下客户端工具,主要用于kubernetes应用chart的创建/打包/发布已经创建和管理和远程Chart仓库。Tiller:helm的服务端,部署于kubernetes内,Tiller接受helm的请求,并根据...

一 基本概念

helm 类似于Linux系统下的包管理器,如yum/apt等,可以方便快捷的将之前打包好的yaml文件快速部署进kubernetes内,方便管理维护。

  • helm:一个命令行下客户端工具,主要用于kubernetes应用chart的创建/打包/发布已经创建和管理和远程Chart仓库。

  • Tiller:helm的服务端,部署于kubernetes内,Tiller接受helm的请求,并根据chart生成kubernetes部署文件(helm称为release),然后提交给 Kubernetes 创建应用。Tiller 还提供了 Release 的升级、删除、回滚等一系列功能。

  • Chart: helm的软件包,采用tar格式,其中包含运行一个应用所需的所有镜像/依赖/资源定义等,还可能包含kubernetes集群中服务定义

  • Release:在kubernetes中集群中运行的一个Chart实例,在同一个集群上,一个Chart可以安装多次,每次安装均会生成一个新的release。

  • Repository:用于发布和存储Chart的仓库

简单来说:

  • helm的作用:像centos7中的yum命令一样,管理软件包,只不过helm这儿管理的是在k8s上安装的各种容器。

  • tiller的作用:像centos7的软件仓库一样,简单说类似于/etc/yum.repos.d目录下的xxx.repo。

二 组件架构



三 工作原理

3.1 Chart install

  • helm从制定目录或tar文件解析chart结构信息

  • helm将制定的chart结构和value信息通过gRPC协议传递给tiller

  • tiller根据chart和values生成一个release

  • tiller通过json将release发送给kubernetes,生成release

3.2 Chart update

  • helm从制定的目录或tar文件解析chart结构信息

  • helm将制定的chart结构和value信息通过gRPC协议传给tiller

  • tiller生成release并更新制定名称的release的history

  • tiller将release信息发送给kubernetes用于更新release

3.3 Chart Rollback

  • helm将会滚的release名称传递给tiller

  • tiller根据release名称查找history

  • tiller从history中获取到上一个release

  • tiller将上一个release发送给kubernetes用于替换当前release

3.4 Chart处理依赖

Tiller 在处理 Chart 时,直接将 Chart 以及其依赖的所有 Charts 合并为一个 Release,同时传递给 Kubernetes。因此 Tiller 并不负责管理依赖之间的启动顺序。Chart 中的应用需要能够自行处理依赖关系。

四 安装部署

4.1 v2版本安装

4.1.1 安装helm

# 在helm客户端主机上,一般为master主机 wget https://get.helm.sh/helm-v2.14.2-linux-amd64.tar.gz tar xf helm-v2.14.2-linux-amd64.tar.gz mv helm /usr/local/bin/ helm version 复制代码

4.1.2 初始化tiller

  • 初始化tiller会自动读取~/.kube目录,所以需要确保config文件存在并认证成功

  • tiller配置rbac,新建rabc-config.yaml并应用

# 在:https://github.com/helm/helm/blob/master/docs/rbac.md 可以找到rbac-config.yaml cat > rbac-config.yaml <<EOF apiVersion: v1 kind: ServiceAccount metadata:   name: tiller   namespace: kube-system --- apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRoleBinding metadata:   name: tiller roleRef:   apiGroup: rbac.authorization.k8s.io   kind: ClusterRole   name: cluster-admin subjects:   - kind: ServiceAccount     name: tiller     namespace: kube-system EOF kubectl apply -f rbac-config.yaml 复制代码
  • 制定镜像

docker pull jessestuart/tiller:v2.14.2 yum install socat  # yum install socat docker tag jessestuart/tiller:v2.14.2 gcr.io/kubernetes-helm/tiller:v2.14.2 helm init -i gcr.io/kubernetes-helm/tiller:v2.9.0  # 需要注意点参数 –client-only:也就是不安装服务端应用,这在 CI&CD 中可能需要,因为通常你已经在 k8s 集群中安装好应用了,这时只需初始化 helm 客户端即可; –history-max:最大历史,当你用 helm 安装应用的时候,helm 会在所在的 namespace 中创建一份安装记录,随着更新次数增加,这份记录会越来越多; –tiller-namespace:默认是 kube-system,你也可以设置为其它 namespace; 复制代码
  • 修改镜像

# 由于gfw原因,可以利用此镜像https://hub.docker.com/r/jessestuart/tiller/tags kubectl edit deployment -n kube-system tiller-deploy image: jessestuart/tiller:v2.14.0 复制代码
  • 异常处理

Error: Looks like "https://kubernetes-charts.storage.googleapis.com" is not a valid chart repository or cannot be reached: Get https://kubernetes-charts.storage.googleapis.com/index.yaml: read tcp 10.2.8.44:49020->216.58.220.208:443: read: connection reset by peer 解决方案:更换源:helm repo add stable https://kubernetes.oss-cn-hangzhou.aliyuncs.com/charts 然后在helm init 注意:tiller可能运行在node节点,将tiller镜像下载到node节点并修改tag 复制代码
  • 查看版本

[root@master ~]# helm version Client: &version.Version{SemVer:"v2.14.2", GitCommit:"a8b13cc5ab6a7dbef0a58f5061bcc7c0c61598e7", GitTreeState:"clean"} Server: &version.Version{SemVer:"v2.14.2+unreleased", GitCommit:"d953c6875cfd4b351a1e8205081ea8aabad7e7d4", GitTreeState:"dirty"} 复制代码

4.2 helm3 安装部署

由于国外很多镜像网站国内无法访问,例如gcr.io ,建议使用阿里源,developer.aliyun.com/hub。

AppHub 是一个托管在国内公有云上、全公益性的 Helm Hub “中国站”,它的后端由阿里云容器平台团队的三位工程师利用 20% 时间开发完成。

而这个站点的一个重要职责,就是把所有 Helm 官方 Hub 托管的应用自动同步到国内;同时,自动将 Charts 文件中的 gcr.io 等所有有网络访问问题的 URL 替换成为稳定的国内镜像 URL。

目前helm3已经不依赖于tiller,Release 名称可在不同 ns 间重用。

4.2.1 安装helm

Helm3 不需要安装tiller,下载到 Helm 二进制文件直接解压到 $PATH 下就可以使用了。

cd /opt && wget https://cloudnativeapphub.oss-cn-hangzhou.aliyuncs.com/helm-v3.0.0-alpha.1-linux-amd64.tar.gz tar -xvf helm-v3.0.0-alpha.1-linux-amd64.tar.gz mv linux-amd64 helm3 mv helm3/helm helm3/helm3 chown root.root helm3 -R cat > /etc/profile.d/helm3.sh << EOF export PATH=$PATH:/opt/helm3 EOF source /etc/profile.d/helm3.sh [root@master helm3]# helm3 version version.BuildInfo{Version:"v3.0.0-alpha.1", GitCommit:"b9a54967f838723fe241172a6b94d18caf8bcdca", GitTreeState:"clean"} 复制代码

4.2.2 使用helm3安装应用

helm repo add apphub https://apphub.aliyuncs.com helm search guestbook helm install guestbook apphub/guestbook 复制代码

五 使用

5.1 基础命令

http://hub.kubeapps.com/ completion  # 为指定的shell生成自动完成脚本(bash或zsh) create      # 创建一个具有给定名称的新 chart delete      # 从 Kubernetes 删除指定名称的 release dependency  # 管理 chart 的依赖关系 fetch       # 从存储库下载 chart 并(可选)将其解压缩到本地目录中 get         # 下载一个命名 release help        # 列出所有帮助信息 history     # 获取 release 历史 home        # 显示 HELM_HOME 的位置 init        # 在客户端和服务器上初始化Helm inspect     # 检查 chart 详细信息 install     # 安装 chart 存档 lint        # 对 chart 进行语法检查 list        # releases 列表 package     # 将 chart 目录打包成 chart 档案 plugin      # 添加列表或删除 helm 插件 repo        # 添加列表删除更新和索引 chart 存储库 reset       # 从集群中卸载 Tiller rollback    # 将版本回滚到以前的版本 search      # 在 chart 存储库中搜索关键字 serve       # 启动本地http网络服务器 status      # 显示指定 release 的状态 template    # 本地渲染模板 test        # 测试一个 release upgrade     # 升级一个 release verify      # 验证给定路径上的 chart 是否已签名且有效 version     # 打印客户端/服务器版本信息 dep         # 分析 Chart 并下载依赖 复制代码
  • 指定value.yaml部署一个chart

helm install --name els1 -f values.yaml stable/elasticsearch 复制代码
  • 升级一个chart

helm upgrade --set mysqlRootPassword=passwd db-mysql stable/mysql helm upgrade go2cloud-api-doc go2cloud-api-doc/  复制代码
  • 回滚一个 chart

helm rollback db-mysql 1 复制代码
  • 删除一个 release

helm delete --purge db-mysql 复制代码
  • 只对模板进行渲染然后输出,不进行安装

helm install/upgrade xxx --dry-run --debug 复制代码

5.2 Chart文件组织

myapp/                               # Chart 目录 ├── charts                           # 这个 charts 依赖的其他 charts,始终被安装 ├── Chart.yaml                       # 描述这个 Chart 的相关信息、包括名字、描述信息、版本等 ├── templates                        # 模板目录 │   ├── deployment.yaml              # deployment 控制器的 Go 模板文件 │   ├── _helpers.tpl                 # 以 _ 开头的文件不会部署到 k8s 上,可用于定制通用信息 │   ├── ingress.yaml                 # ingress 的模板文件 │   ├── NOTES.txt                    # Chart 部署到集群后的一些信息,例如:如何使用、列出缺省值 │   ├── service.yaml                 # service 的 Go 模板文件 │   └── tests │       └── test-connection.yaml └── values.yaml                      # 模板的值文件,这些值会在安装时应用到 GO 模板生成部署文件 复制代码

5.3 新建自己的Chart

  • 创建自己的mychart

[root@master mychart]# helm create mychart Creating mychart [root@master mychart]# ls mychart [root@master mychart]# tree mychart/ mychart/ ├── charts ├── Chart.yaml ├── templates │   ├── deployment.yaml # 部署相关资源 │   ├── _helpers.tpl # 模版助手 │   ├── ingress.yaml # ingress资源 │   ├── NOTES.txt # chart的帮助文本,运行helm install展示给用户 │   ├── service.yaml # service端点 │   └── tests │       └── test-connection.yaml └── values.yaml 3 directories, 8 files 复制代码
  • 删除template下的所有文件,并创建configmap

rm -rf mychart/templates/* # 我们首先创建一个名为 mychart/templates/configmap.yaml: apiVersion: v1 kind: ConfigMap metadata:   name: mychart-configmap data:   myvalue: "Hello World" 复制代码
  • 安装测试

由于创建的yaml文件在template下,tiller读取此文件,会将其发送给kubernetes。

[root@master mychart]# helm install ./mychart/ NAME:   enervated-dolphin LAST DEPLOYED: Sun Jul 21 09:29:13 2019 NAMESPACE: default STATUS: DEPLOYED RESOURCES: ==> v1/ConfigMap NAME               DATA  AGE mychart-configmap  1     0s [root@master mychart]# kubectl get cm mychart-configmap NAME                DATA   AGE mychart-configmap   1      2m6s [root@master mychart]# kubectl describe cm mychart-configmap Name:         mychart-configmap Namespace:    default Labels:       <none> Annotations:  <none> Data ==== myvalue: ---- this is my chart configmap Events:  <none> [root@master mychart]# helm get manifest enervated-dolphin --- # Source: mychart/templates/configmap.yaml apiVersion: v1 kind: ConfigMap metadata:   name: mychart-configmap data:   myvalue: "this is my chart configmap" 复制代码

helm get manifest 命令获取 release 名称(enervated-dolphin)并打印出上传到服务器的所有 Kubernetes 资源。每个文件都以 --- 开始作为 YAML 文档的开始,然后是一个自动生成的注释行,告诉我们该模板文件生成的这个 YAML 文档。

从那里开始,我们可以看到 YAML 数据正是我们在我们的 configmap.yaml 文件中所设计的 。

现在我们可以删除我们的 release:helm delete enervated-dolphin

[root@master mychart]# helm delete enervated-dolphin release "enervated-dolphin" deleted 复制代码

5.4 添加模版调用

硬编码 name: 成资源通常被认为是不好的做法。名称应该是唯一的一个版本。所以我们可能希望通过插入 release 名称来生成一个名称字段。

提示: name: 由于 DNS 系统的限制,该字段限制为 63 个字符。因此,release 名称限制为 53 个字符。Kubernetes 1.3 及更早版本仅限于 24 个字符(即 14 个字符名称)。

修改下之前的configmap为如下内容

apiVersion: v1 kind: ConfigMap metadata:   name: {{.Release.Name}}-configmap data:   myvalue: "Hello World" 复制代码

name: 现在这个值发生了变化成了 {{.Release.Name}}-configmap

模板指令包含在 {{}} 块中。

模板指令 {{.Release.Name}} 将 release 名称注入模板。传递给模板的值可以认为是 namespace 对象,其中 dot(.)分隔每个 namespace 元素。

Release 前面的前一个小圆点表示我们从这个范围的最上面的 namespace 开始(我们将稍微谈一下 scope)。所以我们可以这样理解 .Release.Name:"从顶层命名空间开始,找到 Release 对象,然后在里面查找名为 Name 的对象"。

该 Release 对象是 Helm 的内置对象之一,稍后我们将更深入地介绍它。但就目前而言,这足以说明这会显示 Tiller 分配给我们发布的 release 名称。

现在,当我们安装我们的资源时,我们会立即看到使用这个模板指令的结果:

[root@master mychart]# helm install ./mychart/ NAME:   famous-peahen LAST DEPLOYED: Sun Jul 21 09:42:05 2019 NAMESPACE: default STATUS: DEPLOYED RESOURCES: ==> v1/ConfigMap NAME                    DATA  AGE famous-peahen-confgmap  1     0s [root@master mychart]# helm get manifest famous-peahen --- # Source: mychart/templates/configmap.yaml apiVersion: v1 kind: ConfigMap metadata:   name: famous-peahen-confgmap data:   myvalue: "this is my chart configmap" 复制代码

我们看过了基础的模板:YAML 文件嵌入了模板指令,通过 。在下一部分中,我们将深入研究模板。但在继续之前,有一个快速技巧可以使构建模板更快:当您想测试模板渲染,但实际上没有安装任何东西时,可以使用 helm install --debug --dry-run ./mychart。这会将 chart 发送到 Tiller 服务器,它将渲染模板。但不是安装 chart,它会将渲染模板返回,以便可以看到输出:

六 实战

6.1 制作charts

  • 将用slate做好的go2cloud-api-doc 利用helm做成charts,方便后续部署

helm create go2cloud-api-doc [root@master go2cloud-api-doc]# tree  . ├── charts ├── Chart.yaml ├── templates │   ├── deployment.yaml │   ├── _helpers.tpl │   ├── NOTES.txt │   ├── service.yaml │   └── tests │       └── test-connection.yaml └── values.yaml 3 directories, 8 files  # 配置 deployment [root@master go2cloud_api_doc_charts]# egrep "^$|^#" -v go2cloud-api-doc/templates/deployment.yaml   apiVersion: apps/v1 kind: Deployment metadata:   name: {{ include "go2cloud-api-doc.fullname" . }}   labels: {{ include "go2cloud-api-doc.labels" . | indent 4 }} spec:   replicas: {{ .Values.replicaCount }}   selector:     matchLabels:       app.kubernetes.io/name: {{ include "go2cloud-api-doc.name" . }}       app.kubernetes.io/instance: {{ .Release.Name }}   template:     metadata:       labels:         app.kubernetes.io/name: {{ include "go2cloud-api-doc.name" . }}         app.kubernetes.io/instance: {{ .Release.Name }}     spec:       imagePullSecrets:          - name: {{ .Values.imagePullSecrets }}       containers:         - name: {{ .Chart.Name }}           image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"           imagePullPolicy: {{ .Values.image.pullPolicy }}           ports:             - name: http               containerPort: {{ .Values.service.port }}               protocol: TCP           livenessProbe:             {{- toYaml .Values.livenessProbe | nindent 12  }}           readinessProbe:             {{- toYaml .Values.readinessProbe | nindent 12  }}           resources:             {{- toYaml .Values.resources | nindent 12 }}       {{- with .Values.nodeSelector }}       nodeSelector:         {{- toYaml . | nindent 8 }}       {{- end }}     {{- with .Values.affinity }}       affinity:         {{- toYaml . | nindent 8 }}     {{- end }}     {{- with .Values.tolerations }}       tolerations:         {{- toYaml . | nindent 8 }}     {{- end }}  # 配置service [root@master go2cloud_api_doc_charts]# egrep "^$|^#" -v go2cloud-api-doc/templates/service.yaml  apiVersion: v1 kind: Service metadata:   name: {{ include "go2cloud-api-doc.fullname" . }}   labels: {{ include "go2cloud-api-doc.labels" . | indent 4 }} spec:   type: {{ .Values.service.type }}   ports:     - port: {{ .Values.service.port }}       targetPort: {{ .Values.service.port }}       protocol: TCP       name: http       nodePort: {{ .Values.service.nodePort }}         selector:     app.kubernetes.io/name: {{ include "go2cloud-api-doc.name" . }}     app.kubernetes.io/instance: {{ .Release.Name }}  # 配置values [root@master go2cloud_api_doc_charts]# egrep "^$|^#|^[[:space:]]+#" -v go2cloud-api-doc/values.yaml replicaCount: 1 image:   repository: 10.234.2.218/go2cloud/go2cloud-api-doc   tag: latest   pullPolicy: Always imagePullSecrets: registry-secret nameOverride: "" fullnameOverride: "" service:   type: NodePort   port: 4567   nodePort: 30567 ingress:   enabled: false   annotations: {}   hosts:     - host: chart-example.local       paths: []   tls: [] resources:    requests:     cpu: 1000m     memory: 1280Mi   limits:     cpu: 1000m     memory: 1280Mi livenessProbe:   tcpSocket:     port: 4567   initialDelaySeconds: 10   failureThreshold: 2    timeoutSeconds: 10 readinessProbe:   httpGet:     path: /#introduction     port: http   initialDelaySeconds: 5   failureThreshold: 2    timeoutSeconds: 30 nodeSelector: {} tolerations: [] affinity: {} [root@master go2cloud_api_doc_charts]# egrep "^$|^#|^[[:space:]]+#" -v go2cloud-api-doc/Chart.yaml  apiVersion: v1 appVersion: "1.0" description: A Helm chart for Kubernetes name: go2cloud-api-doc version: 0.1.0  # 部署 [root@master go2cloud_api_doc_charts]# helm install -n go2cloud-api-doc -f go2cloud-api-doc/values.yaml go2cloud-api-doc/                   NAME:   go2cloud-api-doc LAST DEPLOYED: Wed Jul 31 14:34:21 2019 NAMESPACE: default STATUS: DEPLOYED RESOURCES: ==> v1/Deployment NAME              READY  UP-TO-DATE  AVAILABLE  AGE go2cloud-api-doc  0/1    1           0          0s ==> v1/Pod(related) NAME                               READY  STATUS             RESTARTS  AGE go2cloud-api-doc-7cfb7bb795-clrz8  0/1    ContainerCreating  0         0s ==> v1/Service NAME              TYPE      CLUSTER-IP     EXTERNAL-IP  PORT(S)         AGE go2cloud-api-doc  NodePort  10.96.228.251  <none>       4567:30567/TCP  0s NOTES: 1. Get the application URL by running these commands:   export NODE_PORT=$(kubectl get --namespace default -o jsonpath="{.spec.ports[0].nodePort}" services go2cloud-api-doc)   export NODE_IP=$(kubectl get nodes --namespace default -o jsonpath="{.items[0].status.addresses[0].address}")   echo http://$NODE_IP:$NODE_PORT [root@master go2cloud_api_doc_charts]# helm ls go2cloud-api-doc NAME                    REVISION        UPDATED                         STATUS          CHART                   APP VERSION     NAMESPACE go2cloud-api-doc        1               Wed Jul 31 14:34:21 2019        DEPLOYED        go2cloud-api-doc-0.1.0  1.0             default   [root@master go2cloud_api_doc_charts]# kubectl get deployment go2cloud-api-doc NAME               READY   UP-TO-DATE   AVAILABLE   AGE go2cloud-api-doc   0/1     1            0           10m [root@master go2cloud_api_doc_charts]# kubectl get pods |grep go2cloud-api-doc go2cloud-api-doc-7cfb7bb795-clrz8                         0/1     CrashLoopBackOff   7          10m [root@master go2cloud_api_doc_charts]# kubectl get svc go2cloud-api-doc NAME               TYPE       CLUSTER-IP      EXTERNAL-IP   PORT(S)          AGE go2cloud-api-doc   NodePort   10.96.228.251   <none>        4567:30567/TCP   10m  # 打包 [root@master go2cloud_api_doc_charts]# helm package ./go2cloud-api-doc/ Successfully packaged chart and saved it to: /data/go2cloud_api_doc_charts/go2cloud-api-doc-0.1.0.tgz [root@master go2cloud_api_doc_charts]# tree  . ├── go2cloud-api-doc │   ├── charts │   ├── Chart.yaml │   ├── templates │   │   ├── deployment.yaml │   │   ├── _helpers.tpl │   │   ├── NOTES.txt │   │   ├── service.yaml │   │   └── tests │   │       └── test-connection.yaml │   └── values.yaml └── go2cloud-api-doc-0.1.0.tgz 4 directories, 8 files  # 升级副本数量 helm upgrade go2cloud-api-doc --set replicaCount=2 go2cloud-api-doc/ 复制代码



6.2 配置minior

将制作好的charts存放到minio上,在k8s内部署minior

  • 创建本地chart目录

mkdir minio-chart 复制代码
  • 将修改好的chart文件打包

helm package redis 复制代码
  • 将包拷贝至创建的本地chart目录中

cp redis-8.0.5.tgz /root/minio-chart/ 复制代码
  • 更新/root/minio-chart/目录下的index索引

helm repo index minio-chart/ --url http://10.234.2.204:31311/minio/common-helm-repo/ 复制代码


1


  • 将index.yaml 和chart包上传至minio

mc cp index.yaml minio/common-helm-repo/ mc cp redis-8.0.5.tgz minio/common-helm-repo/ 复制代码
  • 将制作好的charts上传至minio

helm repo add monocular https://helm.github.io/monocular helm install -n monocular monocular/monocular mc cp go2cloud-api-doc-0.1.0.tgz minio/common-helm-repo 复制代码


1


可以在${HOME}/.mc/config.json中查看ak密钥信息。

  • 验证



6.3 上传至公共的helm仓库

将制作好的charts包可以上传至helm仓库,可以放在自己的自建私有仓库,流入:kubeapps/Monocular/minior等,可以利用helm命令一键安装。

上传至公有云公共仓库,例如国内的阿里目前创建的Apphub等,在现今的云原生生态当中,已经有很多成熟的开源软件被制作成了 Helm Charts,使得用户可以非常方便地下载和使用,比如 Nginx,Apache、Elasticsearch、Redis 等等。不过,在开放云原生应用中心 App hub(Helm Charts 中国站) 发布之前,国内用户一直都很难直接下载使用这些 Charts。而现在,AppHub 不仅为国内用户实时同步了官方 Helm Hub 里的所有应用,还自动替换了这些 Charts 里所有不可访问的镜像 URL(比如 gcr.io, quay.io 等),终于使得国内开发者通过 helm install “一键安装”应用成为了可能。

具体提交自己的charts可以参考:github.com/cloudnative…

此为我上传的slate chart,Slate helps you create beautiful, intelligent, responsive API documentation.

developer.aliyun.com/hub/detail?…


1


欢迎点赞。

七 相关链接



【版权声明】本文为华为云社区用户原创内容,转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息, 否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。